Merge branch 'master' of github.com:alteriks/kanidm

This commit is contained in:
Krzysztof Dajka 2025-02-11 11:50:55 +01:00
commit 48f7324080
205 changed files with 7218 additions and 6958 deletions

View file

@ -2,9 +2,21 @@
root = true
[*.md]
charset = utf-8
end_of_line = lf
indent_size = 2
max_line_length = 100
trim_trailing_whitespace = true
[*.js]
tab_width = 4
max_line_length = 120
print_width = 120
[*.mjs]
tab_width = 4
max_line_length = 120
print_width = 120

View file

@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install dependencies
@ -27,11 +27,13 @@ jobs:
sudo apt-get update && \
sudo apt-get install -y \
libpam0g-dev \
libudev-dev \
libselinux1-dev \
libssl-dev \
libsystemd-dev \
libtss2-dev \
libudev-dev \
pkg-config \
tpm-udev \
libtss2-dev
tpm-udev
- name: "Run clippy"
run: cargo clippy --lib --bins --examples --all-features
fmt:
@ -39,7 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: "Run cargo fmt"

View file

@ -71,7 +71,8 @@ jobs:
with:
name: kanidm-docker
path: /tmp
- name: Set up ORAS
uses: oras-project/setup-oras@v1
- name: Push image to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \

View file

@ -88,7 +88,8 @@ jobs:
with:
name: kanidmd-docker
path: /tmp
- name: Set up ORAS
uses: oras-project/setup-oras@v1
- name: Push image to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \

View file

@ -70,7 +70,8 @@ jobs:
with:
name: radius-docker
path: /tmp
- name: Set up ORAS
uses: oras-project/setup-oras@v1
# Docker won't directly import OCI images and keep their multi-arch
# features, but ORAS will: https://oras.land/docs/commands/oras_copy
- name: Push image to GHCR

21
.github/workflows/javascript_lint.yml vendored Normal file
View file

@ -0,0 +1,21 @@
---
name: Javascript Linting
"on":
- push
- pull_request
jobs:
javascript_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ESLint to check Javascript files
run:
make eslint
javascript_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Prettier to check Javascript files
run:
make prettier

View file

@ -24,7 +24,7 @@ jobs:
with:
ref: ${{ inputs.tag }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install deps

View file

@ -27,7 +27,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
@ -37,7 +37,8 @@ jobs:
sudo apt-get install -y \
libpam0g-dev \
libudev-dev \
libssl-dev
libssl-dev \
libsystemd-dev
- name: "Build the workspace"
run: cargo build --workspace
@ -74,7 +75,7 @@ jobs:
with:
toolchain: ${{ matrix.rust_version }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
@ -84,7 +85,8 @@ jobs:
sudo apt-get install -y \
libpam0g-dev \
libudev-dev \
libssl-dev
libssl-dev \
libsystemd-dev
- name: "Build the workspace"
run: cargo build --workspace
@ -116,7 +118,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
@ -127,6 +129,7 @@ jobs:
libpam0g-dev \
libudev-dev \
libssl-dev \
libsystemd-dev \
ripgrep
- name: "Run the release build test script"
env:

View file

@ -28,7 +28,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.6
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- run: cargo build -p kanidm_client -p kanidm_tools --bin kanidm

9
.gitignore vendored
View file

@ -17,9 +17,12 @@ tools/orca/example_profiles/small/orca-edited.toml
/docs/
# webui things we don't need
*.d.ts
server/web_ui/*/pkg/*.js
# coverage-related things
*.profraw
tarpaulin-report.html
# kanidm simple packaging
deployment-config/
kanidm_simple_pkg/
@ -35,9 +38,13 @@ pykanidm/site/
# oauth2 integration test things
scripts/oauth_proxy/client.secret
scripts/oauth_proxy/envfile
# local config things
.envrc
# IDEs
.idea/
.vscode/
# javascript test things
node_modules/

View file

@ -1,2 +1,4 @@
# yale-mistakes were made
/pykanidm/* @yaleman
# Least qualified nix guy :P
shell.nix @cebbinghaus

View file

@ -45,6 +45,9 @@
- Chris Olstrom (colstrom)
- Christopher-Robin (cebbinghaus)
- Krzysztof Dajka (alteriks)
- Fabian Kammel (datosh)
- Andris Raugulis (arthepsy)
- Jason (argonaut0)
## Acknowledgements

1716
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
[workspace.package]
version = "1.5.0-dev"
version = "1.6.0-dev"
authors = [
"William Brown <william@blackhats.net.au>",
"James Hodgkinson <james@terminaloutcomes.com>",
]
rust-version = "1.79"
rust-version = "1.80"
edition = "2021"
license = "MPL-2.0"
homepage = "https://github.com/kanidm/kanidm/"
@ -120,27 +120,29 @@ codegen-units = 256
# kanidm-hsm-crypto = { path = "../hsm-crypto" }
[workspace.dependencies]
kanidmd_core = { path = "./server/core", version = "=1.5.0-dev" }
kanidmd_lib = { path = "./server/lib", version = "=1.5.0-dev" }
kanidmd_lib_macros = { path = "./server/lib-macros", version = "=1.5.0-dev" }
kanidmd_testkit = { path = "./server/testkit", version = "=1.5.0-dev" }
kanidm_build_profiles = { path = "./libs/profiles", version = "=1.5.0-dev" }
kanidm_client = { path = "./libs/client", version = "=1.5.0-dev" }
kanidm-hsm-crypto = "^0.2.0"
kanidm_lib_crypto = { path = "./libs/crypto", version = "=1.5.0-dev" }
kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "=1.5.0-dev" }
kanidm_proto = { path = "./proto", version = "=1.5.0-dev" }
kanidm_unix_common = { path = "./unix_integration/common", version = "=1.5.0-dev" }
kanidm_utils_users = { path = "./libs/users", version = "=1.5.0-dev" }
scim_proto = { path = "./libs/scim_proto", version = "=1.5.0-dev" }
sketching = { path = "./libs/sketching", version = "=1.5.0-dev" }
libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" }
anyhow = { version = "1.0.93" }
[workspace.dependencies]
kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" }
kanidmd_lib = { path = "./server/lib", version = "=1.6.0-dev" }
kanidmd_lib_macros = { path = "./server/lib-macros", version = "=1.6.0-dev" }
kanidmd_testkit = { path = "./server/testkit", version = "=1.6.0-dev" }
kanidm_build_profiles = { path = "./libs/profiles", version = "=1.6.0-dev" }
kanidm_client = { path = "./libs/client", version = "=1.6.0-dev" }
kanidm-hsm-crypto = "^0.2.0"
kanidm_lib_crypto = { path = "./libs/crypto", version = "=1.6.0-dev" }
kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "=1.6.0-dev" }
kanidm_proto = { path = "./proto", version = "=1.6.0-dev" }
kanidm_unix_common = { path = "./unix_integration/common", version = "=1.6.0-dev" }
kanidm_utils_users = { path = "./libs/users", version = "=1.6.0-dev" }
scim_proto = { path = "./libs/scim_proto", version = "=1.6.0-dev" }
sketching = { path = "./libs/sketching", version = "=1.6.0-dev" }
anyhow = { version = "1.0.95" }
argon2 = { version = "0.5.3", features = ["alloc"] }
askama = { version = "0.12.1", features = ["serde", "with-axum"] }
askama_axum = { version = "0.4.0" }
async-trait = "^0.1.83"
async-trait = "^0.1.85"
axum = { version = "0.7.9", features = [
"form",
"json",
@ -154,30 +156,30 @@ axum = { version = "0.7.9", features = [
axum-htmx = { version = "0.5.0", features = ["serde", "guards"] }
base32 = "^0.5.1"
base64 = "^0.22.1"
base64urlsafedata = "0.5.0"
bitflags = "^2.6.0"
base64urlsafedata = "0.5.1"
bitflags = "^2.8.0"
bytes = "^1.9.0"
clap = { version = "^4.5.21", features = ["derive", "env"] }
clap_complete = "^4.5.38"
clap = { version = "^4.5.27", features = ["derive", "env"] }
clap_complete = "^4.5.42"
# Forced by saffron/cron
chrono = "^0.4.35"
chrono = "^0.4.39"
compact_jwt = { version = "^0.4.2", default-features = false }
concread = "^0.5.3"
cron = "0.12.1"
cron = "0.15.0"
crossbeam = "0.8.4"
csv = "1.3.1"
dialoguer = "0.10.4"
dialoguer = "0.11.0"
dhat = "0.3.3"
dyn-clone = "^1.0.17"
fernet = "^0.2.1"
filetime = "^0.2.24"
fs4 = "^0.8.3"
fs4 = "^0.12.0"
futures = "^0.3.31"
futures-util = { version = "^0.3.30", features = ["sink"] }
gix = { version = "0.64.0", default-features = false }
hashbrown = { version = "0.14.3", features = ["serde", "inline-more", "ahash"] }
hex = "^0.4.3"
http = "1.1.0"
http = "1.2.0"
hyper = { version = "1.5.1", features = [
"full",
] } # hyper full includes client/server/http2
@ -196,10 +198,10 @@ lazy_static = "^1.5.0"
ldap3_client = "^0.5.2"
ldap3_proto = { version = "^0.5.2", features = ["serde"] }
libc = "^0.2.167"
libc = "^0.2.168"
libnss = "^0.8.0"
libsqlite3-sys = "^0.25.2"
lodepng = "3.10.7"
lodepng = "3.11.0"
lru = "^0.12.5"
mathru = "^0.13.0"
mimalloc = "0.1.43"
@ -207,31 +209,32 @@ notify-debouncer-full = { version = "0.1" }
num_enum = "^0.5.11"
oauth2_ext = { version = "^4.4.2", package = "oauth2", default-features = false }
openssl-sys = "^0.9"
openssl = "^0.10.68"
openssl = "^0.10.70"
opentelemetry = { version = "0.20.0" }
opentelemetry_api = { version = "0.20.0", features = ["logs", "metrics"] }
opentelemetry-otlp = { version = "0.13.0", default-features = false, features = [
opentelemetry = { version = "0.27.0" }
opentelemetry_api = { version = "0.27.0", features = ["logs", "metrics"] }
opentelemetry-otlp = { version = "0.27.0", default-features = false, features = [
"serde",
"logs",
"metrics",
"http-proto",
"grpc-tonic",
] }
opentelemetry_sdk = "0.20.0"
tracing-opentelemetry = "0.21.0"
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio"] }
opentelemetry-semantic-conventions = "0.27.0"
tracing-opentelemetry = "0.28.0"
tracing-core = "0.1.33"
paste = "^1.0.14"
peg = "0.8"
pkg-config = "^0.3.31"
prctl = "1.0.0"
proc-macro2 = "1.0.92"
proc-macro2 = "1.0.93"
qrcode = "^0.12.0"
quote = "1"
rand = "^0.8.5"
rand_chacha = "0.3.1"
regex = "1.11.0"
reqwest = { version = "0.12.9", default-features = false, features = [
reqwest = { version = "0.12.12", default-features = false, features = [
"cookies",
"http2",
"json",
@ -239,17 +242,17 @@ reqwest = { version = "0.12.9", default-features = false, features = [
"rustls-tls-native-roots",
] }
rusqlite = { version = "^0.28.0", features = ["array", "bundled"] }
rustls = { version = "0.23.19", default-features = false, features = [
rustls = { version = "0.23.21", default-features = false, features = [
"aws_lc_rs",
] }
sd-notify = "^0.4.3"
sd-notify = "^0.4.5"
selinux = "^0.4.6"
serde = "^1.0.215"
serde = "^1.0.217"
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.133"
serde_json = "^1.0.137"
serde_urlencoded = "^0.7.1"
serde_with = "3.11.0"
serde_with = "3.12.0"
sha-crypt = "0.5.0"
sha2 = "0.10.8"
shellexpand = "^2.1.2"
@ -258,14 +261,14 @@ smolset = "^1.3.1"
sshkey-attest = "^0.5.0"
sshkeys = "0.3.3"
svg = "0.13.1"
syn = { version = "2.0.90", features = ["full"] }
tempfile = "3.14.0"
syn = { version = "2.0.96", features = ["full"] }
tempfile = "3.15.0"
testkit-macros = { path = "./server/testkit-macros" }
time = { version = "^0.3.34", features = ["formatting", "local-offset"] }
time = { version = "^0.3.36", features = ["formatting", "local-offset"] }
tokio = "^1.41.1"
tokio = "^1.43.0"
tokio-openssl = "^0.6.5"
tokio-util = "^0.7.12"
tokio-util = "^0.7.13"
toml = "^0.5.11"
tracing = { version = "^0.1.41", features = [
@ -279,16 +282,15 @@ url = "^2.5.2"
urlencoding = "2.1.3"
utoipa = { version = "4.2.0", features = ["url", "uuid"] }
utoipa-swagger-ui = "6.0.0"
uuid = "^1.11.0"
uuid = "^1.12.1"
webauthn-authenticator-rs = { version = "0.5.0", features = [
webauthn-authenticator-rs = { version = "0.5.1", features = [
"softpasskey",
"softtoken",
"mozilla",
] }
webauthn-rs = { version = "0.5.0", features = ["preview-features"] }
webauthn-rs-core = "0.5.0"
webauthn-rs-proto = "0.5.0"
webauthn-rs = { version = "0.5.1", features = ["preview-features"] }
webauthn-rs-core = "0.5.1"
webauthn-rs-proto = "0.5.1"
whoami = "^1.5.2"
walkdir = "2"
@ -298,4 +300,3 @@ x509-cert = "0.2.5"
zxcvbn = "^2.2.2"
nonempty = "0.8.1"

View file

@ -178,9 +178,8 @@ codespell:
--skip='*.svg' \
--skip='*.br' \
--skip='./rlm_python/mods-available/eap' \
--skip='./server/lib/src/constants/system_config.rs'
--skip='./pykanidm/site' \
--skip='./server/lib/src/constants/*.json'
--skip='./server/lib/src/constants/system_config.rs' \
--skip='./pykanidm/site'
.PHONY: test/pykanidm/pytest
test/pykanidm/pytest: ## python library testing
@ -314,26 +313,44 @@ cert/clean:
rm -f /tmp/kanidm/ca.txt*
rm -f /tmp/kanidm/ca.{cnf,srl,srl.old}
.PHONY: rust/coverage
coverage/test: ## Run coverage tests
coverage/test:
LLVM_PROFILE_FILE="$(PWD)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test $(TESTS)
.PHONY: coverage/grcov
coverage/grcov: ## Run grcov
coverage/grcov:
rm -rf ./target/coverage/html
grcov . --binary-path ./target/debug/deps/ \
-s . \
-t html \
--branch \
--ignore-not-existing \
--ignore '../*' \
--ignore "/*" \
--ignore "target/*" \
-o target/coverage/html
.PHONY: coverage
coverage: ## Run all the coverage tests
coverage: coverage/test coverage/grcov
echo "Coverage report is in ./target/coverage/html/index.html"
coverage: ## Run the coverage tests using cargo-tarpaulin
cargo tarpaulin --out Html
@echo "Coverage file at file://$(PWD)/tarpaulin-report.html"
.PHONY: coveralls
coveralls: ## Run cargo tarpaulin and upload to coveralls
coveralls:
cargo tarpaulin --coveralls $(COVERALLS_REPO_TOKEN)
@echo "Coveralls repo information is at https://coveralls.io/github/kanidm/kanidm"
.PHONY: eslint
eslint: ## Run eslint on the UI javascript things
eslint: eslint/setup
@echo "################################"
@echo " Running eslint..."
@echo "################################"
cd server/core && find ./static -name '*js' -not -path '*/external/*' -exec eslint "{}" \;
@echo "################################"
@echo "Done!"
.PHONY: eslint/setup
eslint/setup: ## Install eslint for the UI javascript things
cd server/core && npm ci
.PHONY: prettier
prettier: ## Run prettier on the UI javascript things
prettier: eslint/setup
@echo " Running prettier..."
cd server/core && npm run prettier
@echo "Done!"
.PHONY: prettier/fix
prettier/fix: ## Run prettier on the UI javascript things and write back changes
prettier/fix: eslint/setup
@echo " Running prettier..."
cd server/core && npm run prettier:fix
@echo "Done!"

View file

@ -1,8 +1,6 @@
# Kanidm - Simple and Secure Identity Management
<p align="center">
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
</p>
![Kanidm Logo](artwork/logo-small.png)
## About

View file

@ -1,42 +1,93 @@
<p align="center">
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
</p>
# Kanidm Release Notes
# Getting Started
![Kanidm Logo](artwork/logo-small.png)
## Getting Started
To get started, see the [kanidm book]
# Feedback
## Feedback
We value your feedback! First, please see our [code of conduct]. If you have questions please join
our [gitter community channel] so that we can help. If you find a bug or issue, we'd love you to
report it to our [issue tracker].
# Release Notes
## Release Notes
## 2024-11-01 - Kanidm 1.4.0
### 2025-02-09 - Kanidm 1.5.0
This is the latest stable release of the Kanidm Identity Management project. Every release is the
combined effort of our community and we appreciate their invaluable contributions, comments,
questions, feedback and support.
You should review our
[support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md) as this
[support documentation] as this
may have important effects on your distribution or upgrades in future.
Before upgrading you should review
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes)
[our upgrade documentation]
### 1.4.0 Important Changes
#### 1.5.0 Important Changes
- There has been a lot of tweaks to how cookies are handled in this release, if you're having issues with the login flow please clear all cookies as an initial troubleshooting step.
#### 1.5.0 Release Highlights
- Many updates to the UI!
- SSH Keys in Credentials Update (#3027)
- Improved error message when PassKey is missing PIN (mainly for Firefox) (#3403)
- Fix the password reset form and possible resolver issue (#3398)
- Fixed unrecoverable error page doesn't include logo or domain name (#3352)
- Add support for prefers-color-scheme using Bootstrap classes. Dark mode! (#3327)
- Automatically trigger passkeys on login view (#3307)
- Two new operating systems!
- Initial OpenBSD support (#3381)
- FreeBSD client (#3333)
- Many SCIM-related improvements
- SCIM access control (#3359)
- SCIM put (#3151)
- OAuth2 Things
- Allow OAuth2 with empty `state` parameter (#3396)
- Allow POST on oauth userinfo (#3395)
- Add OAuth2 `response_mode=fragment` (#3335)
- Add CORS headers to jwks and userinfo (#3283)
- Allowing SPN query with non-SPN structured data in LDAP (#3400)
- Correctly return that uuid2spn changed on domain rename (#3402)
- RADIUS startup fixing (#3388)
- Repaired systemd reload notifications (#3355)
- Add `ssh_publickeys` as a claim for OAuth2 (#3346)
- Allow modification of password minimum length (#3345)
- PAM on Debian, enable use_first_pass by default (#3326)
- Allow opt-in of easter eggs (#3308)
- Allow reseting account policy values to defaults (#3306)
- Ignore system users for UPG synthesiseation (#3297)
- Allow group managers to modify entry-managed-by (#3272)
And many more!
### 2024-11-01 - Kanidm 1.4.0
This is the latest stable release of the Kanidm Identity Management project. Every release is the
combined effort of our community and we appreciate their invaluable contributions, comments,
questions, feedback and support.
You should review our
[support documentation] as this
may have important effects on your distribution or upgrades in future.
Before upgrading you should review
[our upgrade documentation]
#### 1.4.0 Important Changes
- The web user interface has been rewritten and now supports theming. You will notice that your
domain displayname is included in a number of locations on upgrade, and that you can set
your own domain and OAuth2 client icons.
- OAuth2 strict redirect uri is now required. Ensure you have read
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes).
[our upgrade documentation].
and taken the needed steps before upgrading.
### 1.4.0 Release Highlights
#### 1.4.0 Release Highlights
- Improve handling of client timeouts when the server is under high load
- Resolve a minor issue preventing some credential updates from saving
@ -65,20 +116,20 @@ and taken the needed steps before upgrading.
- Rewrite the entire web frontend to be simpler and faster, allowing more features to be added
in the future. Greatly improves user experience as the pages are now very fast to load!
## 2024-08-07 - Kanidm 1.3.0
### 2024-08-07 - Kanidm 1.3.0
This is the latest stable release of the Kanidm Identity Management project. Every release is the
combined effort of our community and we appreciate their invaluable contributions, comments,
questions, feedback and support.
You should review our
[support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md) as this
[support documentation] as this
may have important effects on your distribution or upgrades in future.
Before upgrading you should review
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes)
[our upgrade documentation]
### 1.3.0 Important Changes
#### 1.3.0 Important Changes
- New GID number constraints are now enforced in this version. To upgrade from 1.2.0 all accounts
and groups must adhere to these rules. See [our upgrade documentation]. about tools to help you
@ -89,7 +140,7 @@ Before upgrading you should review
by PassKeys which give a better user experience.
- Kanidm now supports FreeBSD and Illumos in addition to Linux
### 1.3.0 Release Highlights
#### 1.3.0 Release Highlights
- TOTP update user interface improvements
- Improved error messages when a load balancer is failing
@ -112,24 +163,24 @@ Before upgrading you should review
- Strict redirect URI enforcement in OAuth2
- Substring indexing for improved search performance
## 2024-05-01 - Kanidm 1.2.0
### 2024-05-01 - Kanidm 1.2.0
This is the first stable release of the Kanidm Identity Management project. We want to thank every
one in our community who has supported to the project to this point with their invaluable
contributions, comments, questions, feedback and support.
Importantly this release makes a number of changes to our project's support processes. You should
review our [support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md)
review our [support documentation]
as this may have important effects on your distribution or upgrades in future.
### 1.2.0 Important Changes
#### 1.2.0 Important Changes
- On upgrade all OAuth2 sessions and user sessions will be reset due to changes in cryptographic key
handling. This does not affect api tokens.
- There is a maximum limit of 48 interactive sessions for persons where older sessions are
automatically removed.
### 1.2.0 Release Highlights
#### 1.2.0 Release Highlights
- The book now contains a list of supported RFCs and standards
- Add code challenge methods to OIDC discovery
@ -154,7 +205,7 @@ as this may have important effects on your distribution or upgrades in future.
- Migrate cryptographic key handling to an object model with future HSM support
- Limit maximum active sessions on an account to 48
## 2024-02-07 - Kanidm 1.1.0-rc.16
### 2024-02-07 - Kanidm 1.1.0-rc.16
This is the sixteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
help get feedback and ideas from the community on how we can continue to make this project better.
@ -163,7 +214,7 @@ This is the final release candidate before we publish a release version. We beli
server interfaces are stable and reliable enough for people to depend on, and to develop external
tools to interact with Kanidm.
### 1.1.0-rc.16 Release Highlights
#### 1.1.0-rc.16 Release Highlights
- Replication for two node environments is now supported
- Account policy supports password minimum length
@ -182,7 +233,7 @@ tools to interact with Kanidm.
- Support RFC6749 Client Credentials Grant
- Support custom claim maps in OIDC
## 2023-10-31 - Kanidm 1.1.0-beta14
### 2023-10-31 - Kanidm 1.1.0-beta14
This is the fourteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
help get feedback and ideas from the community on how we can continue to make this project better.
@ -191,7 +242,7 @@ At this point we believe we are on the final stretch to making something we cons
ready". After this we will start to ship release candidates as our focus will now be changing to
finish our production components and the stability of the API's for longer term support.
### 1.1.0-beta14 Release Highlights
#### 1.1.0-beta14 Release Highlights
- Replication is in Beta! Please test carefully!
- Web UI WASM has been split up, significantly improving the responsiveness.
@ -205,7 +256,7 @@ finish our production components and the stability of the API's for longer term
- Removed a lot of uses of `unwrap` and `expect` to improve reliability.
- Account policy framework is now in place.
## 2023-05-01 - Kanidm 1.1.0-beta13
### 2023-05-01 - Kanidm 1.1.0-beta13
This is the thirteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
help get feedback and ideas from the community on how we can continue to make this project better.
@ -214,7 +265,7 @@ At this point we believe we are on the final stretch to making something we cons
ready". After this we will start to ship release candidates as our focus will now be changing to
finish our production components and the stability of the API's for longer term support.
### 1.1.0-beta13 Release Highlights
#### 1.1.0-beta13 Release Highlights
- Replication foundations
- Full implementation of replication refresh
@ -255,7 +306,7 @@ finish our production components and the stability of the API's for longer term
- Improve create-reset-token user experience
- Improve self-healing for some reference issues
## 2023-05-01 - Kanidm 1.1.0-alpha12
### 2023-05-01 - Kanidm 1.1.0-alpha12
This is the twelfth alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
@ -266,7 +317,7 @@ done so yet is we haven't decided if we want to commit to the current API layout
There are still things we want to change there. Otherwise the server is stable and reliable for
production usage.
### Release Highlights
#### 1.1.0-alpha12 Release Highlights
- Allow full server content replication in testing (yes we're finally working on replication!)
- Improve OAuth2 to allow scoped members to see RS they can access for UI flows
@ -286,7 +337,7 @@ production usage.
- Add exclusive process lock to daemon
- Allow dns/rdns in ldap search contexts
## 2023-02-01 - Kanidm 1.1.0-alpha11
### 2023-02-01 - Kanidm 1.1.0-alpha11
This is the eleventh alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
@ -296,7 +347,7 @@ The project is shaping up very nicely, and a beta will be coming soon! The main
done so yet is we haven't decided if we want to commit to the current API layout and freeze it yet.
There are still things we want to change there. Otherwise the server is stable and reliable.
### Release Highlights
#### 1.1.0-alpha11 Release Highlights
- Support /etc/skel home dir templates in kanidm-unixd
- Improve warning messages for openssl when a cryptographic routine is not supported
@ -317,7 +368,7 @@ There are still things we want to change there. Otherwise the server is stable a
- Improve the access control module to evaluate access in a clearer way
- Allow synced users to correct modify their local sessions
## 2022-11-01 - Kanidm 1.1.0-alpha10
### 2022-11-01 - Kanidm 1.1.0-alpha10
This is the tenth alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
@ -325,12 +376,12 @@ for a future supported release.
The project is shaping up very nicely, and a beta will be coming soon!
### Upgrade Note
#### 1.1.0-alpha10 Upgrade Note
This version will _require_ TLS on all servers, even if behind a load balancer or TLS terminating
proxy. You should be ready for this change when you upgrade to the latest version.
### Release Highlights
#### 1.1.0-alpha10 Release Highlights
- Management and tracking of authenticated sessions
- Make upgrade migrations more robust when upgrading over multiple versions
@ -352,7 +403,7 @@ proxy. You should be ready for this change when you upgrade to the latest versio
- Cleanup of expired authentication sessions
- Improved administration of password badlists
## 2022-08-02 - Kanidm 1.1.0-alpha9
### 2022-08-02 - Kanidm 1.1.0-alpha9
This is the ninth alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
@ -360,7 +411,7 @@ for a future supported release.
The project is shaping up very nicely, and a beta will be coming soon!
### Release Highlights
#### 1.1.0-alpha9 Release Highlights
- Inclusion of a Python3 API library
- Improve orca usability
@ -376,13 +427,13 @@ The project is shaping up very nicely, and a beta will be coming soon!
- CTAP2+ support in Webauthn via CLI
- Radius supports EAP TLS identities in addition to EAP PEAP
## 2022-05-01 - Kanidm 1.1.0-alpha8
### 2022-05-01 - Kanidm 1.1.0-alpha8
This is the eighth alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
better for a future supported release.
### Release Highlights
#### 1.1.0-alpha8 Release Highlights
- Foundations for cryptographic trusted device authentication
- Foundations for new user onboarding and credential reset
@ -398,13 +449,13 @@ better for a future supported release.
- Highlight that the WebUI is in alpha to prevent confusion
- Remove sync only client paths
## 2022-01-01 - Kanidm 1.1.0-alpha7
### 2022-01-01 - Kanidm 1.1.0-alpha7
This is the seventh alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
better for a future supported release.
### Release Highlights
#### 1.1.0-alpha7 Release Highlights
- OAuth2 scope to group mappings
- Webauthn subdomain support
@ -415,7 +466,7 @@ better for a future supported release.
- Addition of email address attributes
- Web UI improvements for OAuth2
## 2021-10-01 - Kanidm 1.1.0-alpha6
### 2021-10-01 - Kanidm 1.1.0-alpha6
This is the sixth alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
@ -424,7 +475,7 @@ for a future supported release.
It's also a special release as Kanidm has just turned 3 years old! Thank you all for helping to
bring the project this far! 🎉 🦀
### Release Highlights
#### 1.1.0-alpha6 Release Highlights
- Support backup codes as MFA in case of lost TOTP/Webauthn
- Dynamic menus on CLI for usernames when multiple sessions exist
@ -444,13 +495,13 @@ bring the project this far! 🎉 🦀
- Improvements to performance with high cache sizes
- Session tokens persist over a session restart
## 2021-07-07 - Kanidm 1.1.0-alpha5
### 2021-07-07 - Kanidm 1.1.0-alpha5
This is the fifth alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
for a future supported release.
### Release Highlights
#### 1.1.0-alpha5 Release Highlights
- Fix a major defect in how backup/restore worked
- Improve query performance by caching partial queries
@ -465,13 +516,13 @@ for a future supported release.
- Statistical analysis of indexes to improve query optimisation
- Handle broken TOTP authenticator apps
## 2021-04-01 - Kanidm 1.1.0-alpha4
### 2021-04-01 - Kanidm 1.1.0-alpha4
This is the fourth alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
better for a future supported release.
### Release Highlights
#### 1.1.0-alpha4 Release Highlights
- Performance Improvements
- TOTP CLI enrollment
@ -485,13 +536,13 @@ better for a future supported release.
- Badlist checked at login to determine account compromise
- Minor Fixes for attribute display
## 2021-01-01 - Kanidm 1.1.0-alpha3
### 2021-01-01 - Kanidm 1.1.0-alpha3
This is the third alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
for a future supported release.
### Release Highlights
#### 1.1.0-alpha3 Release Highlights
- Account "valid from" and "expiry" times.
- Rate limiting and softlocking of account credentials to prevent bruteforcing.
@ -499,13 +550,13 @@ for a future supported release.
- Rewrite of json authentication protocol components.
- Unixd will cache "non-existent" items to improve nss/pam latency.
## 2020-10-01 - Kanidm 1.1.0-alpha2
### 2020-10-01 - Kanidm 1.1.0-alpha2
This is the second alpha series release of the Kanidm Identity Management project. Alpha releases
are to help get feedback and ideas from the community on how we can continue to make this project
better for a future supported release.
### Release Highlights
#### 1.1.0-alpha2 Release Highlights
- SIMD key lookups in container builds for datastructures
- Server and Client hardening warnings for running users and file permissions
@ -517,7 +568,7 @@ better for a future supported release.
- Reduction in memory footprint during searches
- Change authentication from cookies to auth-bearer tokens
## 2020-07-01 - Kanidm 1.1.0-alpha1
### 2020-07-01 - Kanidm 1.1.0-alpha1
This is the first alpha series release of the Kanidm Identity Management project. Alpha releases are
to help get feedback and ideas from the community on how we can continue to make this project better
@ -536,7 +587,7 @@ people. I would especially like to thank:
- Samuel Cabrero (scabrero)
- Jim McDonough
### Release Highlights
#### 1.1.0-alpha1 Release Highlights
- A working identity management server, including database
- RADIUS authentication and docker images
@ -552,3 +603,5 @@ people. I would especially like to thank:
[gitter community channel]: https://gitter.im/kanidm/community
[code of conduct]: https://github.com/kanidm/kanidm/blob/master/CODE_OF_CONDUCT.md
[kanidm book]: https://kanidm.github.io/kanidm/stable/
[our upgrade documentation]: https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes
[support documentation]: https://github.com/kanidm/kanidm/blob/master/book/src/support.md

View file

@ -40,9 +40,9 @@
- [Service Integrations](integrations/readme.md)
- [LDAP](integrations/ldap.md)
- [OAuth2](integrations/oauth2.md)
- [How does OAuth2 work?](integrations/oauth2/how_does_oauth2_work.md)
- [Custom Claims](integrations/oauth2/custom_claims.md)
- [Example Configurations](integrations/oauth2/examples.md)
- [How does OAuth2 work?](integrations/oauth2/how_does_oauth2_work.md)
- [PAM and nsswitch](integrations/pam_and_nsswitch.md)
- [SUSE / OpenSUSE](integrations/pam_and_nsswitch/suse.md)
- [Fedora](integrations/pam_and_nsswitch/fedora.md)

View file

@ -31,6 +31,8 @@ weakest to strongest:
- `passkey`
- `attested_passkey`
`attested_passkey` requires [configuring an allowlist of trusted authenticators](#setting-webauthn-attestation-ca-lists).
### Password Minimum Length
The minimum length for passwords (if they are allowed).
@ -45,7 +47,7 @@ read/write session.
The list of certificate authorities and device aaguids that must be used by members of this policy.
This allows limiting devices to specific models.
To generate this list you should use `fido-mds-tool`.
To generate this list you should [use `fido-mds-tool`](#setting-webauthn-attestation-ca-lists).
## Policy Resolution
@ -149,15 +151,59 @@ kanidm group account-policy privilege-expiry my_admin_group 86400 # NB: will be
### Setting Webauthn Attestation CA Lists
The list should be generated with `fido-mds-tool`. This will emit JSON that can be directly used
with Kanidm.
To verify Webauthn authenticators with attestation, Kanidm needs an allowlist of
authenticators to trust. Generate this list with the `fido-mds-tool` from
the [webauthn-rs project](https://github.com/kanidm/webauthn-rs). If you have a
Rust toolchain installed, it can built and installed from source with
```bash
kanidm group account-policy webauthn-attestation-ca-list <group name> <attestation ca list json>
kanidm group account-policy webauthn-attestation-ca-list idm_all_persons '{"cas":{"D6E4b4Drh .... }'
cargo install fido-mds-tool
```
> NOTE: `fido-mds-tool` is available in the `kanidm:tools` container.
Alternatively, `fido-mds-tool` is available in the
[tools container](../installing_client_tools.md#tools-container).
First, fetch the MDS data provided by the FIDO Alliance:
```bash
fido-mds-tool fetch
```
Then, query the MDS data to generate your allowlist of authenticators.
For example, to trust all authenticators made by Yubico, run
```bash
fido-mds-tool query --output-cert-roots "desc cnt yubikey" > trusted-authenticators
```
For details of how to query the MDS data, run
```bash
fido-mds-tool query --help
```
Once you have generated the authenticator allowlist, use it to configure Kanidm's
account policy for a group. For example, to set the allowlist for all persons, run
```bash
kanidm group account-policy webauthn-attestation-ca-list idm_all_persons trusted-authenticators
```
### Setting Primary Credential Fallback
The primary credential fallback enables behavior which allows authenticating
using the primary account password when logging in via LDAP.
If both an LDAP and primary password are specified, Kanidm will only accept the LDAP password.
```bash
kanidm group account-policy allow-primary-cred-fallback <group name> <enabled>
```
to disable it for a group you would run:
```bash
kanidm group account-policy allow-primary-cred-fallback <group name> false
```
## Global Settings

View file

@ -114,3 +114,7 @@ When a service like sudo, sshd, su, etc. wants to authenticate someone, it opens
that service, then performs authentication according to the modules defined in the pam.d config. For
example, if you run `ls -al /etc/pam.d /usr/etc/pam.d` in SUSE, you can see the services and their
respective pam.d config.
## Test coverage
We're trying to regularly get coverage reports into [Coveralls](https://coveralls.io/github/kanidm/kanidm), you can run the local testing with `make coverage` once you've installed [cargo-tarpaulin](https://crates.io/crates/cargo-tarpaulin).

View file

@ -103,7 +103,7 @@ You will need [rustup](https://rustup.rs/) to install a Rust toolchain.
You will need to install rustup and our build dependencies with:
```bash
zypper in rustup git libudev-devel sqlite3-devel libopenssl-3-devel libselinux-devel pam-devel tpm2-0-tss-devel
zypper in rustup git libudev-devel sqlite3-devel libopenssl-3-devel libselinux-devel pam-devel systemd-devel tpm2-0-tss-devel
```
You can then use rustup to complete the setup of the toolchain.
@ -157,7 +157,7 @@ You need [rustup](https://rustup.rs/) to install a Rust toolchain.
You will also need some system libraries to build this, which can be installed by running:
```bash
sudo apt-get install libudev-dev libssl-dev pkg-config libpam0g-dev
sudo apt-get install libudev-dev libssl-dev libsystemd-dev pkg-config libpam0g-dev
```
Tested with Ubuntu 20.04 and 22.04.

View file

@ -3,57 +3,58 @@
## Pre-Reqs
```bash
cargo install cargo-audit
cargo install cargo-outdated
cargo install cargo-udeps
cargo install cargo-machete
cargo install --force \
cargo-audit \
cargo-outdated \
cargo-udeps \
cargo-machete
```
## Pre Release Check List
### Start a release
- [ ] git checkout -b YYYYMMDD-pre-release
- [ ] `git checkout -b "$(date +%Y%m%d)-pre-release"`
### Cargo Tasks
- [ ] Update MSRV if applicable
- [ ] cargo update
- [ ] `cargo update`
- [ ] `RUSTC_BOOTSTRAP=1 cargo udeps`
- [ ] `cargo machete`
- [ ] cargo outdated -R
- [ ] cargo audit
- [ ] cargo test
- [ ] `cargo machete --with-metadata`
- [ ] `cargo outdated -R`
- [ ] `cargo audit`
- [ ] `cargo test`
- [ ] setup a local instance and run orca (TBD)
- [ ] store a copy an an example db (TBD)
### Code Changes
- [ ] upgrade crypto policy values if required
- [ ] upgrade crypto policy values if required (see `libs/crypto/src/lib.rs` -> `CryptoPolicy`)
- [ ] check for breaking db entry changes.
### Administration
- [ ] Update `RELEASE_NOTES.md`
- [ ] Update `README.md`
- [ ] cargo test
- [ ] git commit -a -m "Release Notes"
- [ ] git push origin YYYYMMDD-pre-release
- [ ] `cargo test`
- [ ] `git commit -a -m 'chore: Release Notes'`
- [ ] `git push origin "$(date +%Y%m%d)-pre-release"`
- [ ] Merge PR
### Git Management
- [ ] git checkout master
- [ ] git pull
- [ ] `git checkout master`
- [ ] `git pull`
- [ ] git checkout -b 1.x.0 (Note no v to prevent ref conflict)
- [ ] update version to set pre tag in ./Cargo.toml
- [ ] git commit -m "Release 1.x.0-pre"
- [ ] git tag v1.x.0-pre
- [ ] `git commit -m "Release $(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')-pre"`
- [ ] `git tag v$(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')-pre`
- [ ] Final inspect of the branch
- [ ] git push origin 1.x.0 --tags
- [ ] `git push origin "$(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')" --tags`
- [ ] github -> Ensure release branch is protected
@ -106,4 +107,3 @@ cargo install cargo-machete
### Distro
- [ ] vendor and release to build.opensuse.org

View file

@ -145,7 +145,8 @@ with a dn of `dn=token` and provide the api token in the password.
> [!NOTE]
>
> The `dn=token` keyword is guaranteed to not be used by any other entry, which is why it was chosen
> as the keyword to initiate api token binds.
> as the keyword to initiate api token binds. Additionally it is not required, leaving the field empty
> will fall back to the service-account if a "password" is provided
```bash
ldapwhoami -H ldaps://URL -x -D "dn=token" -w "TOKEN"
@ -234,6 +235,7 @@ ldapwhoami ... -x -D '22a65b6c-80c8-4e1a-9b76-3f3afdff8400'
ldapwhoami ... -x -D 'spn=test1@idm.example.com,dc=idm,dc=example,dc=com'
ldapwhoami ... -x -D 'name=test1,dc=idm,dc=example,dc=com'
```
<sub>in fact, the key of the bind isn't used at all so `googoogaaga=test1` is entirely valid</sub> ;)
## Troubleshooting

View file

@ -70,6 +70,31 @@ anything special for Kanidm (or another provider).
**Note:** some apps automatically append `/.well-known/openid-configuration` to
the end of an OIDC Discovery URL, so you may need to omit that.
<dl>
<dt>[Webfinger](https://datatracker.ietf.org/doc/html/rfc7033) URL</dt>
<dd>
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
The webfinger URL is implemented for each OpenID client, under its specific endpoint, giving full control to the administrator regarding which to use.
To make this compliant with the standard, it must be made available under the correct [well-known endpoint](https://datatracker.ietf.org/doc/html/rfc7033#section-10.1) (e.g `example.com/.well-known/webfinger`), typically via a reverse proxy or similar. Kanidm doesn't currently provide a mechanism for this URI rewrite.
One example would be dedicating one client as the "primary" or "default" and redirecting all requests to that. Alternatively, source IP or other request metadata could be used to decide which client to forward the request to.
### Caddy
`Caddyfile`
```caddy
# assuming a kanidm service with domain "example.com"
example.com {
redir /.well-known/webfinger https://idm.example.com/oauth2/openid/:client_id:{uri} 307
}
```
**Note:** the `{uri}` is important as it preserves the original request past the redirect.
</dd>
<dt>
@ -229,6 +254,10 @@ kanidm system oauth2 update-scope-map nextcloud nextcloud_users email profile op
> * **address** - address
> * **phone** - phone_number, phone_number_verified
> * **groups** - groups
>
> In addition Kanidm supports some vendor specific scopes that can include additional claims.
>
> * **ssh_publickeys** - array of ssh_publickey of the user
<!-- this is just to split the templates up -->

View file

@ -54,6 +54,99 @@ In the virtual host, to protect a location/directory
</Directory>
```
## Gitea
[Gitea](https://docs.gitea.com/) is a painless, self-hosted, all-in-one software
development service. It has built in support for
[external authentication](https://docs.gitea.com/administration/authentication)
including OAuth2.
To set up a Gitea instance to authenticate with Kanidm:
1. Add an email address to your regular Kanidm account, if it doesn't have one
already:
```sh
kanidm person update your_username -m your_username@example.com
```
2. Create a new Kanidm group for your Gitea users (`gitea_users`), and add your
regular account to it:
```sh
kanidm group create gitea_users
kanidm group add-members gitea_users your_username
```
3. Create a new OAuth2 application configuration in Kanidm (`gitea`), configure
the redirect URL, and scope access to the `gitea_users` group:
```sh
kanidm system oauth2 create gitea Gitea https://gitea.example.com/user/login
kanidm system oauth2 add-redirect-url gitea https://gitea.example.com/user/oauth2/kanidm/callback
kanidm system oauth2 update-scope-map gitea gitea_users email openid profile groups
```
4. Gitea currently [does not support PKCE](https://github.com/go-gitea/gitea/issues/21376)
in their OIDC implementation. If you do not perform this step, you will see an error like
`No PKCE code challenge was provided with client in enforced PKCE mode.`
in your Kanidm server logs. Therefore, we have to disable PKCE for Gitea:
```sh
kanidm system oauth2 warning-insecure-client-disable-pkce gitea
```
5. Get the `gitea` OAuth2 client secret from Kanidm:
```sh
kanidm system oauth2 show-basic-secret gitea
```
6. Log in to Gitea with an administrator account and go to Site Administration
-> Identity & Access -> Authentication Sources, and "Add Authentication Source",
then provide the following details:
* **Type**: `OAuth2`
* **Name**: `kanidm`, in case you want to choose a different name, make sure
to update `kanidm` in the redirect URL in step 3. The full redirect URL is
provided at the bottom of the current configuration page in Gitea.
* **OAuth2 Provider**: `OpenID Connect`
* **Client ID (key)**: `gitea`
* **Client Secret**: [from show-basic-secret above]
* **OpenID Connect Auto Discovery URL**: `https://kanidm.example.com/oauth2/openid/gitea/.well-known/openid-configuration`
Alternatively, you can provide the configuration via the CLI:
```sh
gitea admin auth add-oauth \
--provider=openidConnect \
--name=kanidm \
--key=gitea \
--secret=[from show-basic-secret above] \
--auto-discover-url=https://kanidm.example.com/oauth2/openid/gitea/.well-known/openid-configuration \
```
You should now see a "Sign in with Kanidm" button on your Gitea login page.
You may additionally want to configure:
* A Gitea themed icon in Kanidm for the `gitea` OAuth2 application:
```sh
curl -LO https://gitea.example.com/assets/img/logo.svg
kanidm system oauth2 set-image gitea logo.svg svg
rm logo.svg
```
* To disable password authentication in Gitea, add the following
[configuration](https://docs.gitea.com/next/administration/config-cheat-sheet)
to `app.ini`:
```ini
[service]
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
SHOW_REGISTRATION_BUTTON = false
ENABLE_PASSWORD_SIGNIN_FORM = false
```
## GitLab
[GitLab](https://gitlab.com) is a Git-based software development platform, which

View file

@ -103,82 +103,26 @@ kanidm service-account credential generate --name admin radius_service_account
## Deploying a RADIUS Container
We provide a RADIUS container that has all the needed integrations. This container requires some
cryptographic material, with the following files being in `/etc/raddb/certs`. (Modifiable in the
cryptographic material, with the following files mounted in `/data`. (Modifiable in the
configuration)
| filename | description |
| -------- | ------------------------------------------------------------- |
| ca.pem | The signing CA of the RADIUS certificate |
| dh.pem | The output of `openssl dhparam -in ca.pem -out ./dh.pem 2048` |
| cert.pem | The certificate for the RADIUS server |
| key.pem | The signing key for the RADIUS certificate |
| filename | description |
| -------- | ------------------------------------------------------------- |
| ca.pem | The signing CA of the RADIUS certificate |
| cert.pem | The certificate for the RADIUS server |
| key.pem | The private key for the RADIUS certificate |
| radius.toml | The configuration file |
The configuration file (`/data/kanidm`) has the following template:
The configuration file (which you should mount at `/data/radius.toml`, or specify its path with the environment variable `KANIDM_RLM_CONFIG`) has the following template:
```toml
uri = "https://example.com" # URL to the Kanidm server
verify_hostnames = true # verify the hostname of the Kanidm server
verify_ca = false # Strict CA verification
ca = /data/ca.pem # Path to the kanidm ca
auth_token = "ABC..." # Auth token for the service account
# See: kanidm service-account api-token generate
# Default vlans for groups that don't specify one.
radius_default_vlan = 1
# A list of Kanidm groups which must be a member
# before they can authenticate via RADIUS.
radius_required_groups = [
"radius_access_allowed@idm.example.com",
]
# A mapping between Kanidm groups and VLANS
radius_groups = [
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 },
]
# A mapping of clients and their authentication tokens
radius_clients = [
{ name = "test", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
# radius_cert_path = "/etc/raddb/certs/cert.pem"
# the signing key for radius TLS
# radius_key_path = "/etc/raddb/certs/key.pem"
# the diffie-hellman output
# radius_dh_path = "/etc/raddb/certs/dh.pem"
# the CA certificate
# radius_ca_path = "/etc/raddb/certs/ca.pem"
{{#rustdoc_include ../../../examples/radius.toml}}
```
## A fully configured example
```toml
url = "https://example.com"
# The auth token for the service account
auth_token = "ABC..."
# default vlan for groups that don't specify one.
radius_default_vlan = 99
# if the user is in one of these Kanidm groups,
# then they're allowed to authenticate
radius_required_groups = [
"radius_access_allowed@idm.example.com",
]
radius_groups = [
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 }
]
radius_clients = [
{ name = "localhost", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
{{#rustdoc_include ../../../examples/radius_full.toml}}
```
## Moving to Production
@ -200,14 +144,17 @@ the problem. To increase the logging level you can re-run your environment with
```bash
docker rm radiusd
docker run --name radiusd \
-e DEBUG=True \
--rm -e DEBUG=True \
-p 1812:1812 \
-p 1812:1812/udp
-p 1812:1812/udp \
--interactive --tty \
--volume /tmp/kanidm:/etc/raddb/certs \
--mount "type=bind,src=$(pwd)/examples/radius.toml,target=/data/kanidm" \
--mount "type=bind,src=/tmp/kanidm,target=/data" \
kanidm/radius:latest
```
In this example we're running it from the root of the repository and loading an example config, and using the certificates generated in dev-mode. You'll need to adjust your mounts to suit!
Note: the RADIUS container _is_ configured to provide
[Tunnel-Private-Group-ID](https://freeradius.org/rfc/rfc2868.html#Tunnel-Private-Group-ID), so if
you wish to use Wi-Fi-assigned VLANs on your infrastructure, you can assign these by groups in the

View file

@ -33,7 +33,8 @@ You can find the name of your 389 Directory Server instance with:
```bash
# Run on the FreeIPA server
dsconf --list
dsctl --list
> slapd-DEV-KANIDM-COM
```
Using this you can show the current status of the retro changelog plugin to see if you need to
@ -83,6 +84,20 @@ kanidm-ipa-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ipa-sync -n
kanidm-ipa-sync -i /etc/kanidm/ipa-sync -n
```
As the sync tool is part of the tools container, you can run this with:
```bash
docker run --rm -i -t \
--user uid:gid \
-p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/kanidm.ca.pem:/path/to/kanidm.ca.pem:ro
-v /path/to/ipa-ca.pem:/etc/kanidm/ipa-ca.pem:ro \
-v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \
kanidm/tools:latest \
kanidm-ipa-sync -i /etc/kanidm/ipa-sync -
```
## Running the Sync Tool Automatically
The sync tool can be run on a schedule if you configure the `schedule` parameter, and provide the
@ -96,11 +111,14 @@ kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
As the sync tool is part of the tools container, you can run this with:
```bash
docker create --name kanidm-ipa-sync \
docker run --name kanidm-ipa-sync \
--user uid:gid \
-p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/kanidm.ca.pem:/path/to/kanidm.ca.pem:ro
-v /path/to/ipa-ca.pem:/etc/kanidm/ipa-ca.pem:ro \
-v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \
kanidm/tools:latest \
kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
```

Binary file not shown.

View file

@ -17,8 +17,9 @@ sync_token = "eyJhb..."
# server in the IPA topology rather than via a load balancer or dns srv records. This
# is to prevent replication conflicts and issues due to how 389-ds content sync works.
ipa_uri = "ldaps://specific-server.ipa.dev.kanidm.com"
# Path to the IPA CA certificate in PEM format.
ipa_ca = "/path/to/kanidm-ipa-ca.pem"
# Path to the IPA CA certificate in PEM format. This can be found on an IPA server
# in the file `/etc/ipa/ca.crt`
ipa_ca = "/path/to/ipa-ca.pem"
# The DN of an account with content sync rights. By default cn=Directory Manager has
# this access.
ipa_sync_dn = "cn=Directory Manager"

View file

@ -1,7 +1,7 @@
## Kanidm minimal Service Configuration - /etc/kanidm/config
# Kanidm minimal Service Configuration - /etc/kanidm/config
# For a full example and documentation, see /usr/share/kanidm/kanidm
# or `example/kanidm` in the source repository.
# Replace this with your kanidmd URI and uncomment the line
#uri = "https://idm.example.com"
# uri = "https://idm.example.com"
verify_ca = true

18
examples/radius.toml Normal file
View file

@ -0,0 +1,18 @@
uri = "https://example.com"
# The auth token for the service account
auth_token = "ABC..."
# default vlan for groups that don't specify one.
radius_default_vlan = 99
# if the user is in one of these Kanidm groups,
# then they're allowed to authenticate
radius_required_groups = ["radius_access_allowed@idm.example.com"]
radius_groups = [{ spn = "radius_access_allowed@idm.example.com", vlan = 10 }]
radius_clients = [
{ name = "localhost", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker", ipaddr = "172.17.0.0/16", secret = "testing123" },
]

28
examples/radius_full.toml Normal file
View file

@ -0,0 +1,28 @@
uri = "https://example.com" # URL to the Kanidm server
verify_hostnames = true # verify the hostname of the Kanidm server
verify_ca = true # Strict CA verification
auth_token = "ABC..." # Auth token for the service account
# See: kanidm service-account api-token generate
# Default vlans for groups that don't specify one.
radius_default_vlan = 1
# A list of Kanidm groups which must be a member
# before they can authenticate via RADIUS.
radius_required_groups = ["radius_access_allowed@idm.example.com"]
# A mapping between Kanidm groups and VLANS
radius_groups = [{ spn = "radius_access_allowed@idm.example.com", vlan = 10 }]
# A mapping of clients and their authentication tokens
radius_clients = [
{ name = "test", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker", ipaddr = "172.17.0.0/16", secret = "testing123" },
]
# radius_cert_path = "/etc/raddb/certs/cert.pem"
# the signing key for radius TLS
# radius_key_path = "/etc/raddb/certs/key.pem"
radius_ca_path = "/data/ca.pem" # Path to the kanidm ca
# radius_ca_dir = "/data/ca"

View file

@ -1,17 +1,19 @@
## Kanidm Unixd minimal Service Configuration - /etc/kanidm/unixd
# Kanidm Unixd minimal Service Configuration - /etc/kanidm/unixd
# For a full example and documentation, see /usr/share/kanidm-unixd/unixd
# or `example/unixd` in the source repository.
# or `example/unixd` in the source repository
version = '2'
[kanidm]
# default_shell = "/bin/sh"
# home_attr = "uuid"
# home_alias = "spn"
# use_etc_skel = false
# Defines a set of POSIX groups where membership of any of these groups
# will be allowed to login via PAM.
# Replace your group below and uncomment this line:
#pam_allowed_login_groups = ["your_posix_login_group"]
# will be allowed to login via PAM
#
# WITHOUT THIS SET, NOBODY WILL BE ABLE TO LOG IN VIA PAM
#
# Replace your group below and uncomment this line
# pam_allowed_login_groups = ["your_posix_login_group"]

View file

@ -1,4 +1,5 @@
use crate::{ClientError, KanidmClient};
use kanidm_proto::constants::ATTR_DOMAIN_ALLOW_EASTER_EGGS;
use kanidm_proto::internal::ImageValue;
use reqwest::multipart;
@ -8,6 +9,14 @@ impl KanidmClient {
self.perform_delete_request("/v1/domain/_image").await
}
pub async fn idm_set_domain_allow_easter_eggs(&self, enable: bool) -> Result<(), ClientError> {
self.perform_put_request(
&format!("{}{}", "/v1/domain/_attr/", ATTR_DOMAIN_ALLOW_EASTER_EGGS),
vec![enable.to_string()],
)
.await
}
/// Add or update the domain logo/image
pub async fn idm_domain_update_image(&self, image: ImageValue) -> Result<(), ClientError> {
let file_content_type = image.filetype.as_content_type_str();

View file

@ -37,6 +37,14 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_authsession_expiry_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!("/v1/group/{}/_attr/authsession_expiry", id))
.await
}
pub async fn group_account_policy_credential_type_minimum_set(
&self,
id: &str,
@ -61,6 +69,17 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_password_minimum_length_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!(
"/v1/group/{}/_attr/auth_password_minimum_length",
id
))
.await
}
pub async fn group_account_policy_privilege_expiry_set(
&self,
id: &str,
@ -73,6 +92,14 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_privilege_expiry_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!("/v1/group/{}/_attr/privilege_expiry", id))
.await
}
pub async fn group_account_policy_webauthn_attestation_set(
&self,
id: &str,
@ -85,6 +112,17 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_webauthn_attestation_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!(
"/v1/group/{}/_attr/webauthn_attestation_ca_list",
id
))
.await
}
pub async fn group_account_policy_limit_search_max_results(
&self,
id: &str,
@ -97,6 +135,14 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_limit_search_max_results_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!("/v1/group/{}/_attr/limit_search_max_results", id))
.await
}
pub async fn group_account_policy_limit_search_max_filter_test(
&self,
id: &str,
@ -109,6 +155,17 @@ impl KanidmClient {
.await
}
pub async fn group_account_policy_limit_search_max_filter_test_reset(
&self,
id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(&format!(
"/v1/group/{}/_attr/limit_search_max_filter_test",
id
))
.await
}
pub async fn group_account_policy_allow_primary_cred_fallback(
&self,
id: &str,

View file

@ -35,3 +35,7 @@ x509-cert = { workspace = true, features = ["pem"] }
[dev-dependencies]
sketching = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["openssl-sys"]

View file

@ -16,8 +16,5 @@ doctest = false
[dependencies]
[target.'cfg(target_family = "windows")'.dependencies]
whoami = { workspace = true }
[target.'cfg(not(target_family = "windows"))'.dependencies]
kanidm_utils_users = { workspace = true }

View file

@ -3,6 +3,9 @@ use std::fs::Metadata;
#[cfg(target_os = "freebsd")]
use std::os::freebsd::fs::MetadataExt;
#[cfg(target_os = "openbsd")]
use std::os::openbsd::fs::MetadataExt;
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt;

View file

@ -28,3 +28,7 @@ toml = { workspace = true }
[build-dependencies]
base64 = { workspace = true }
gix = { workspace = true, default-features = false }
[package.metadata.cargo-machete]
ignored = ["gix"]

View file

@ -1,6 +1,8 @@
htmx_ui_pkg_path = "/hpkg"
# Don't set the cpu_flags to autodetect for this platform
# cpu_flags = "none"
admin_bind_path = "/data/kanidmd.sock"
default_config_path = "/data/server.toml"
default_unix_shell_path = "/bin/false"
server_admin_bind_path = "/data/kanidmd.sock"
server_ui_pkg_path = "/hpkg"
server_config_path = "/data/server.toml"
client_config_path = "/data/config"
resolver_config_path = "/data/unixd"
resolver_unix_shell_path = "/bin/false"

View file

@ -1,6 +1,8 @@
htmx_ui_pkg_path = "../core/static"
# Set to native for developer machines.
cpu_flags = "native"
admin_bind_path = "/tmp/kanidmd.sock"
default_config_path = "../../examples/insecure_server.toml"
default_unix_shell_path = "/bin/bash"
server_admin_bind_path = "/tmp/kanidmd.sock"
server_ui_pkg_path = "../core/static"
server_config_path = "../../examples/insecure_server.toml"
client_config_path = "/etc/kanidm/config"
resolver_config_path = "/tmp/unixd"
resolver_unix_shell_path = "/bin/bash"

View file

@ -0,0 +1,8 @@
# Don't set the value for autodetect
# cpu_flags = "none"
server_admin_bind_path = "/var/run/kanidmd/sock"
server_ui_pkg_path = "/usr/local/share/kanidm/ui/hpkg"
server_config_path = "/usr/local/etc/kanidm/server.toml"
client_config_path = "/usr/local/etc/kanidm/config"
resolver_config_path = "/usr/local/etc/kanidm/unixd"
resolver_unix_shell_path = "/bin/sh"

View file

@ -1,6 +1,8 @@
htmx_ui_pkg_path = "/usr/share/kanidm/ui/hpkg"
# Don't set the value for autodetect
# cpu_flags = "none"
admin_bind_path = "/var/run/kanidmd/sock"
default_config_path = "/etc/kanidm/server.toml"
default_unix_shell_path = "/bin/bash"
server_admin_bind_path = "/var/run/kanidmd/sock"
server_ui_pkg_path = "/usr/share/kanidm/ui/hpkg"
server_config_path = "/etc/kanidm/server.toml"
client_config_path = "/etc/kanidm/config"
resolver_config_path = "/etc/kanidm/unixd"
resolver_unix_shell_path = "/bin/bash"

View file

@ -54,12 +54,14 @@ impl std::fmt::Display for CpuOptLevel {
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ProfileConfig {
htmx_ui_pkg_path: String,
#[serde(default)]
cpu_flags: CpuOptLevel,
admin_bind_path: String,
default_config_path: String,
default_unix_shell_path: String,
server_admin_bind_path: String,
server_config_path: String,
server_ui_pkg_path: String,
client_config_path: String,
resolver_config_path: String,
resolver_unix_shell_path: String,
}
pub fn apply_profile() {
@ -127,19 +129,27 @@ pub fn apply_profile() {
println!("cargo:rustc-env=KANIDM_PROFILE_NAME={}", profile);
println!("cargo:rustc-env=KANIDM_CPU_FLAGS={}", profile_cfg.cpu_flags);
println!(
"cargo:rustc-env=KANIDM_HTMX_UI_PKG_PATH={}",
profile_cfg.htmx_ui_pkg_path
"cargo:rustc-env=KANIDM_SERVER_UI_PKG_PATH={}",
profile_cfg.server_ui_pkg_path
);
println!(
"cargo:rustc-env=KANIDM_ADMIN_BIND_PATH={}",
profile_cfg.admin_bind_path
"cargo:rustc-env=KANIDM_SERVER_ADMIN_BIND_PATH={}",
profile_cfg.server_admin_bind_path
);
println!(
"cargo:rustc-env=KANIDM_DEFAULT_CONFIG_PATH={}",
profile_cfg.default_config_path
"cargo:rustc-env=KANIDM_SERVER_CONFIG_PATH={}",
profile_cfg.server_config_path
);
println!(
"cargo:rustc-env=KANIDM_DEFAULT_UNIX_SHELL_PATH={}",
profile_cfg.default_unix_shell_path
"cargo:rustc-env=KANIDM_CLIENT_CONFIG_PATH={}",
profile_cfg.client_config_path
);
println!(
"cargo:rustc-env=KANIDM_RESOLVER_CONFIG_PATH={}",
profile_cfg.resolver_config_path
);
println!(
"cargo:rustc-env=KANIDM_RESOLVER_UNIX_SHELL_PATH={}",
profile_cfg.resolver_unix_shell_path
);
}

View file

@ -17,9 +17,8 @@ test = false
doctest = false
[dependencies]
gethostname = "0.5.0"
num_enum = { workspace = true }
opentelemetry = { workspace = true, features = ["metrics", "rt-tokio"] }
opentelemetry = { workspace = true, features = ["metrics"] }
opentelemetry-otlp = { workspace = true, default-features = false, features = [
"serde",
"logs",
@ -27,9 +26,12 @@ opentelemetry-otlp = { workspace = true, default-features = false, features = [
"http-proto",
"grpc-tonic",
] }
opentelemetry_sdk = { workspace = true }
opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] }
opentelemetry-semantic-conventions = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true, features = ["attributes"] }
tracing-core = { workspace = true }
tracing-forest = { workspace = true, features = [
"uuid",
"smallvec",

View file

@ -1,16 +1,26 @@
use gethostname::gethostname;
use opentelemetry::KeyValue;
use std::{str::FromStr, time::Duration};
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::trace::{self, Sampler};
use opentelemetry_sdk::Resource;
use std::time::Duration;
use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
use opentelemetry_sdk::{
trace::{Sampler, TracerProvider},
Resource,
};
use tracing::Subscriber;
use tracing_subscriber::Registry;
use tracing_subscriber::{prelude::*, EnvFilter};
use tracing_core::Level;
use tracing_subscriber::{filter::Directive, prelude::*, EnvFilter, Registry};
pub const MAX_EVENTS_PER_SPAN: u32 = 64 * 1024;
pub const MAX_ATTRIBUTES_PER_SPAN: u32 = 128;
use opentelemetry_semantic_conventions::{
attribute::{SERVICE_NAME, SERVICE_VERSION},
SCHEMA_URL,
};
// TODO: this is coming back later
// #[allow(dead_code)]
// pub fn init_metrics() -> metrics::Result<MeterProvider> {
@ -44,28 +54,26 @@ pub fn start_logging_pipeline(
// adding these filters because when you close out the process the OTLP comms layer is NOISY
let forest_filter = forest_filter
.add_directive(
"tonic=info"
.parse()
.expect("Failed to set tonic logging to info"),
Directive::from_str("tonic=info").expect("Failed to set tonic logging to info"),
)
.add_directive("h2=info".parse().expect("Failed to set h2 logging to info"))
.add_directive(
"hyper=info"
.parse()
.expect("Failed to set hyper logging to info"),
Directive::from_str("h2=info").expect("Failed to set h2 logging to info"),
)
.add_directive(
Directive::from_str("hyper=info").expect("Failed to set hyper logging to info"),
);
let forest_layer = tracing_forest::ForestLayer::default().with_filter(forest_filter);
let t_filter: EnvFilter = EnvFilter::builder()
.with_default_directive(log_filter.into())
.from_env_lossy();
let tracer = opentelemetry_otlp::new_pipeline().tracing().with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(endpoint)
.with_timeout(Duration::from_secs(5))
.with_protocol(Protocol::HttpBinary),
);
let otlp_exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.with_protocol(Protocol::HttpBinary)
.with_timeout(Duration::from_secs(5))
.build()
.map_err(|err| err.to_string())?;
// this env var gets set at build time, if we can pull it, add it to the metadata
let git_rev = match option_env!("KANIDM_PKG_COMMIT_REV") {
@ -74,39 +82,47 @@ pub fn start_logging_pipeline(
};
let version = format!("{}{}", env!("CARGO_PKG_VERSION"), git_rev);
let hostname = gethostname();
let hostname = hostname.to_string_lossy();
let hostname = hostname.to_lowercase();
// let hostname = gethostname::gethostname();
// let hostname = hostname.to_string_lossy();
// let hostname = hostname.to_lowercase();
let tracer = tracer
.with_trace_config(
trace::config()
// we want *everything!*
.with_sampler(Sampler::AlwaysOn)
.with_max_events_per_span(MAX_EVENTS_PER_SPAN)
.with_max_attributes_per_span(MAX_ATTRIBUTES_PER_SPAN)
.with_resource(Resource::new(vec![
KeyValue::new("service.name", service_name),
KeyValue::new("service.version", version),
KeyValue::new("host.name", hostname),
// TODO: it'd be really nice to be able to set the instance ID here, from the server UUID so we know *which* instance on this host is logging
])),
let resource = Resource::from_schema_url(
[
// TODO: it'd be really nice to be able to set the instance ID here, from the server UUID so we know *which* instance on this host is logging
KeyValue::new(SERVICE_NAME, service_name),
KeyValue::new(SERVICE_VERSION, version),
// TODO: currently marked as an experimental flag, leaving it out for now
// KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, hostname),
],
SCHEMA_URL,
);
let provider = TracerProvider::builder()
.with_batch_exporter(otlp_exporter, opentelemetry_sdk::runtime::Tokio)
// we want *everything!*
.with_sampler(Sampler::AlwaysOn)
.with_max_events_per_span(MAX_EVENTS_PER_SPAN)
.with_max_attributes_per_span(MAX_ATTRIBUTES_PER_SPAN)
.with_resource(resource)
.build();
global::set_tracer_provider(provider.clone());
provider.tracer("tracing-otel-subscriber");
use tracing_opentelemetry::OpenTelemetryLayer;
let registry = tracing_subscriber::registry()
.with(
tracing_subscriber::filter::LevelFilter::from_level(Level::INFO)
.with_filter(t_filter),
)
.install_batch(opentelemetry::runtime::Tokio)
.map_err(|err| {
let err = format!("Failed to start OTLP pipeline: {:?}", err);
eprintln!("{}", err);
err
})?;
// Create a tracing layer with the configured tracer;
let telemetry = tracing_opentelemetry::layer()
.with_tracer(tracer)
.with_threads(true)
.with_filter(t_filter);
.with(tracing_subscriber::fmt::layer())
// .with(MetricsLayer::new(meter_provider.clone()))
.with(forest_layer)
.with(OpenTelemetryLayer::new(
provider.tracer("tracing-otel-subscriber"),
));
Ok(Box::new(
Registry::default().with(forest_layer).with(telemetry),
))
Ok(Box::new(registry))
}
None => {
let forest_layer = tracing_forest::ForestLayer::default().with_filter(forest_filter);
@ -122,7 +138,6 @@ pub struct TracingPipelineGuard {}
impl Drop for TracingPipelineGuard {
fn drop(&mut self) {
opentelemetry::global::shutdown_tracer_provider();
opentelemetry::global::shutdown_logger_provider();
eprintln!("Logging pipeline completed shutdown");
}
}

View file

@ -1,13 +1,13 @@
[package]
name = "kanidm_utils_users"
description = "Kanidm utility crate"
version.workspace = true
authors.workspace = true
rust-version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
version = { workspace = true }
authors = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
[lib]
test = true

View file

@ -0,0 +1,4 @@
These are the FreeBSD port Makefiles and other supporting files. In the future we will submit
these to FreeBSD ports rather than maintaining them here.

View file

@ -0,0 +1,49 @@
PORTNAME= kanidm
# DISTVERSION= 1.5.0-dev
# DISTVERSIONPREFIX= v
DISTVERSION= g20250102
GH_TAGNAME= edb8cccc84e9dacd2ac31ea1162dd24c0c454c55
GH_ACCOUNT= Firstyear
CATEGORIES= security net databases
LICENSE= MPL20
LICENSE_FILE= ${WRKSRC}/LICENSE.md
MAINTAINER= william@blackhats.net.au
COMMENT= Simple and secure identity management platform
WWW= https://github.com/kanidm/kanidm/
USES= cargo ssl
USE_GITHUB= yes
ONLY_FOR_ARCHS= aarch64 amd64
CARGO_ENV= KANIDM_BUILD_PROFILE=release_freebsd
CARGO_BUILD_ARGS = -p kanidm_tools -p kanidm_unix_int -p nss_kanidm -p pam_kanidm
CARGO_INSTALL= no
USE_RC_SUBR= kanidm_unixd kanidm_unixd_tasks
USERS= _kanidm_unixd
GROUPS= _kanidm_unixd
do-install:
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm ${STAGEDIR}${PREFIX}/bin
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm-unix ${STAGEDIR}${PREFIX}/bin
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm_ssh_authorizedkeys ${STAGEDIR}${PREFIX}/bin
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm_ssh_authorizedkeys_direct ${STAGEDIR}${PREFIX}/bin
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm_unixd ${STAGEDIR}${PREFIX}/libexec
${INSTALL_PROGRAM} ${WRKDIR}/target/release/kanidm_unixd_tasks ${STAGEDIR}${PREFIX}/libexec
${INSTALL_LIB} ${WRKDIR}/target/release/libnss_kanidm.so ${STAGEDIR}${PREFIX}/lib/nss_kanidm.so.1
${INSTALL_LIB} ${WRKDIR}/target/release/libpam_kanidm.so ${STAGEDIR}${PREFIX}/lib
${MKDIR} ${STAGEDIR}${PREFIX}/etc
${MKDIR} ${STAGEDIR}${PREFIX}/etc/kanidm
${MKDIR} ${STAGEDIR}/var/run/kanidm-unixd
${MKDIR} ${STAGEDIR}/var/lib/kanidm-unixd
${MKDIR} ${STAGEDIR}/var/cache/kanidm-unixd
.include <bsd.port.mk>

View file

@ -0,0 +1,27 @@
#!/bin/sh
# PROVIDE: kanidm_unixd
# REQUIRE: LOGIN
# KEYWORD: shutdown
#
# Add these lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# kanidm_unixd_enable (bool): Set to NO by default.
# Set it to YES to enable kanidm_unixd.
. /etc/rc.subr
name=kanidm_unixd
rcvar=kanidm_unixd_enable
load_rc_config $name
: ${kanidm_unixd_enable:="NO"}
pidfile="/var/run/kanidm-unixd.pid"
command=/usr/sbin/daemon
command_args="-u _kanidm_unixd -p /var/run/kanidm-unixd.pid -T kanidm_unixd /usr/local/libexec/${name}"
procname=/usr/local/libexec/${name}
run_rc_command "$1"

View file

@ -0,0 +1,27 @@
#!/bin/sh
# PROVIDE: kanidm_unixd_tasks
# REQUIRE: LOGIN
# KEYWORD: shutdown
#
# Add these lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# kanidm_unixd_tasks_enable (bool): Set to NO by default.
# Set it to YES to enable kanidm_unixd_tasks.
. /etc/rc.subr
name=kanidm_unixd_tasks
rcvar=kanidm_unixd_tasks_enable
load_rc_config $name
: ${kanidm_unixd_tasks_enable:="NO"}
pidfile="/var/run/kanidm-unixd-tasks.pid"
command=/usr/sbin/daemon
command_args="-u root -p /var/run/kanidm-unixd-tasks.pid -T kanidm_unixd_tasks /usr/local/libexec/${name}"
procname=/usr/local/libexec/${name}
run_rc_command "$1"

View file

@ -0,0 +1 @@
Kanidm is a simple and secure identity provider and client for UNIX systems

View file

@ -0,0 +1,13 @@
bin/kanidm
bin/kanidm-unix
bin/kanidm_ssh_authorizedkeys
bin/kanidm_ssh_authorizedkeys_direct
lib/nss_kanidm.so.1
lib/libpam_kanidm.so
libexec/kanidm_unixd
libexec/kanidm_unixd_tasks
@dir %%ETCDIR%%
@dir /var/lib
@dir(_kanidm_unixd,_kanidm_unixd,750) /var/cache/kanidm-unixd
@dir(_kanidm_unixd,_kanidm_unixd,750) /var/lib/kanidm-unixd
@dir(_kanidm_unixd,_kanidm_unixd,755) /var/run/kanidm-unixd

View file

@ -42,3 +42,6 @@ sshkeys = { workspace = true }
[dev-dependencies]
enum-iterator = { workspace = true }
serde_urlencoded = { workspace = true }
[build-dependencies]
kanidm_build_profiles = { workspace = true }

3
proto/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
profiles::apply_profile();
}

View file

@ -53,6 +53,7 @@ pub enum Attribute {
DisplayName,
Dn,
Domain,
DomainAllowEasterEggs,
DomainDevelopmentTaint,
DomainDisplayName,
DomainLdapBasedn,
@ -282,6 +283,7 @@ impl Attribute {
Attribute::DisplayName => ATTR_DISPLAYNAME,
Attribute::Dn => ATTR_DN,
Attribute::Domain => ATTR_DOMAIN,
Attribute::DomainAllowEasterEggs => ATTR_DOMAIN_ALLOW_EASTER_EGGS,
Attribute::DomainDevelopmentTaint => ATTR_DOMAIN_DEVELOPMENT_TAINT,
Attribute::DomainDisplayName => ATTR_DOMAIN_DISPLAY_NAME,
Attribute::DomainLdapBasedn => ATTR_DOMAIN_LDAP_BASEDN,
@ -464,6 +466,7 @@ impl Attribute {
ATTR_DISPLAYNAME => Attribute::DisplayName,
ATTR_DN => Attribute::Dn,
ATTR_DOMAIN => Attribute::Domain,
ATTR_DOMAIN_ALLOW_EASTER_EGGS => Attribute::DomainAllowEasterEggs,
ATTR_DOMAIN_DISPLAY_NAME => Attribute::DomainDisplayName,
ATTR_DOMAIN_DEVELOPMENT_TAINT => Attribute::DomainDevelopmentTaint,
ATTR_DOMAIN_LDAP_BASEDN => Attribute::DomainLdapBasedn,

View file

@ -30,7 +30,7 @@ pub const VALID_IMAGE_UPLOAD_CONTENT_TYPES: [&str; 5] = [
pub const APPLICATION_JSON: &str = "application/json";
/// The "system" path for Kanidm client config
pub const DEFAULT_CLIENT_CONFIG_PATH: &str = "/etc/kanidm/config";
pub const DEFAULT_CLIENT_CONFIG_PATH: &str = env!("KANIDM_CLIENT_CONFIG_PATH");
/// The user-owned path for Kanidm client config
pub const DEFAULT_CLIENT_CONFIG_PATH_HOME: &str = "~/.config/kanidm";
@ -89,6 +89,7 @@ pub const ATTR_DESCRIPTION: &str = "description";
pub const ATTR_DIRECTMEMBEROF: &str = "directmemberof";
pub const ATTR_DISPLAYNAME: &str = "displayname";
pub const ATTR_DN: &str = "dn";
pub const ATTR_DOMAIN_ALLOW_EASTER_EGGS: &str = "domain_allow_easter_eggs";
pub const ATTR_DOMAIN_DEVELOPMENT_TAINT: &str = "domain_development_taint";
pub const ATTR_DOMAIN_DISPLAY_NAME: &str = "domain_display_name";
pub const ATTR_DOMAIN_LDAP_BASEDN: &str = "domain_ldap_basedn";
@ -218,6 +219,7 @@ pub const ATTR_ALLOW_PRIMARY_CRED_FALLBACK: &str = "allow_primary_cred_fallback"
pub const OAUTH2_SCOPE_EMAIL: &str = ATTR_EMAIL;
pub const OAUTH2_SCOPE_GROUPS: &str = "groups";
pub const OAUTH2_SCOPE_SSH_PUBLICKEYS: &str = "ssh_publickeys";
pub const OAUTH2_SCOPE_OPENID: &str = "openid";
pub const OAUTH2_SCOPE_READ: &str = "read";
pub const OAUTH2_SCOPE_SUPPLEMENT: &str = "supplement";

View file

@ -160,6 +160,7 @@ pub enum CURegWarning {
AttestedResidentKeyRequired,
Unsatisfiable,
WebauthnAttestationUnsatisfiable,
WebauthnUserVerificationRequired,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]

View file

@ -142,6 +142,13 @@ pub enum OperationError {
DatabaseLockAcquisitionTimeout,
// Specific internal errors.
AU0001InvalidState,
AU0002JwsSerialisation,
AU0003JwsSignature,
AU0004UserAuthTokenInvalid,
AU0005DelayedProcessFailure,
AU0006CredentialMayNotReauthenticate,
AU0007UserAuthTokenInvalid,
// Kanidm Generic Errors
KG001TaskTimeout,
@ -152,6 +159,15 @@ pub enum OperationError {
CU0001WebauthnAttestationNotTrusted,
CU0002WebauthnRegistrationError,
CU0003WebauthnUserNotVerified,
// The session is inconsistent and can't be committed, but the errors
// can be resolved.
CU0004SessionInconsistent,
// Another session used this intent token, and so it can't be committed.
CU0005IntentTokenConflict,
// The intent token was invalidated before we could commit.
CU0006IntentTokenInvalidated,
// ValueSet errors
VS0001IncomingReplSshPublicKey,
VS0002CertificatePublicKeyDigest,
@ -259,6 +275,7 @@ pub enum OperationError {
// Web UI
UI0001ChallengeSerialisation,
UI0002InvalidState,
UI0003InvalidOauth2Resume,
// Unixd Things
KU001InitWhileSessionActive,
@ -295,7 +312,7 @@ impl Display for OperationError {
impl OperationError {
/// Return the message associated with the error if there is one.
fn message(&self) -> Option<String> {
pub fn message(&self) -> Option<String> {
match self {
Self::SessionExpired => None,
Self::EmptyRequest => None,
@ -361,9 +378,23 @@ impl OperationError {
Self::TransactionAlreadyCommitted => None,
Self::ValueDenyName => None,
Self::DatabaseLockAcquisitionTimeout => Some("Unable to acquire a database lock - the current server may be too busy. Try again later.".into()),
Self::AU0001InvalidState => Some("Invalid authentication session state for request".into()),
Self::AU0002JwsSerialisation => Some("JWS serialisation failed".into()),
Self::AU0003JwsSignature => Some("JWS signature failed".into()),
Self::AU0004UserAuthTokenInvalid => Some("User auth token was unable to be generated".into()),
Self::AU0005DelayedProcessFailure => Some("Delaying processing failure, unable to proceed".into()),
Self::AU0006CredentialMayNotReauthenticate => Some("Credential may not reauthenticate".into()),
Self::AU0007UserAuthTokenInvalid => Some("User auth token was unable to be generated".into()),
Self::CU0001WebauthnAttestationNotTrusted => None,
Self::CU0002WebauthnRegistrationError => None,
Self::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device.".into()),
Self::CU0004SessionInconsistent => Some("The session is unable to be committed due to unresolved warnings.".into()),
Self::CU0005IntentTokenConflict => Some("The intent token used to create this session has been reused in another browser/tab and may not proceed.".into()),
Self::CU0006IntentTokenInvalidated => Some("The intent token has been invalidated/revoked before the commit could be accepted. Has it been used in another browser or tab?".into()),
Self::DB0001MismatchedRestoreVersion => None,
Self::DB0002MismatchedRestoreVersion => None,
Self::DB0003FilterResolveCacheBuild => None,
@ -443,25 +474,26 @@ impl OperationError {
Self::SC0009IndexTypeSyntaxInvalid => Some("A SCIM IndexType contained invalid syntax".into()),
Self::SC0010DateTimeSyntaxInvalid => Some("A SCIM DateTime contained invalid syntax".into()),
Self::SC0011AddressSyntaxInvalid => Some("A SCIM Address contained invalid syntax".into()),
Self::SC0012CertificateSyntaxInvalid => Some("A SCIM Certificate contained invalid binary data".into()),
Self::SC0013CertificateInvalidDer => Some("A SCIM Certificate did not contain valid DER".into()),
Self::SC0014CertificateInvalidDigest => Some("A SCIM Certificate was unable to be digested".into()),
Self::SC0015CredentialTypeSyntaxInvalid => Some("A SCIM CredentialType contained invalid syntax".into()),
Self::SC0016InameSyntaxInvalid => Some("A SCIM Iname string contained invalid syntax".into()),
Self::SC0017Iutf8SyntaxInvalid => Some("A SCIM Iutf8 string contained invalid syntax".into()),
Self::SC0018NsUniqueIdSyntaxInvalid => Some("A SCIM NsUniqueID contained invalid syntax".into()),
Self::SC0019Oauth2ScopeSyntaxInvalid => Some("A SCIM Oauth2 Scope contained invalid syntax".into()),
Self::SC0020Oauth2ScopeMapSyntaxInvalid => Some("A SCIM Oauth2 Scope Map contained invalid syntax".into()),
Self::SC0021Oauth2ScopeMapMissingGroupIdentifier => Some("A SCIM Oauth2 Scope Map was missing a group name or uuid".into()),
Self::SC0022Oauth2ClaimMapSyntaxInvalid => Some("A SCIM Oauth2 Claim Map contained invalid syntax".into()),
Self::SC0023Oauth2ClaimMapMissingGroupIdentifier => Some("A SCIM Claim Map was missing a group name or uuid".into()),
Self::SC0024SshPublicKeySyntaxInvalid => Some("A SCIM Ssh Public Key contained invalid syntax".into()),
Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()),
Self::SC0026Utf8SyntaxInvalid => Some("A SCIM Utf8 String Scope Map contained invalid syntax".into()),
Self::SC0011AddressSyntaxInvalid => Some("A SCIM Address contained invalid syntax".into()),
Self::SC0012CertificateSyntaxInvalid => Some("A SCIM Certificate contained invalid binary data".into()),
Self::SC0013CertificateInvalidDer => Some("A SCIM Certificate did not contain valid DER".into()),
Self::SC0014CertificateInvalidDigest => Some("A SCIM Certificate was unable to be digested".into()),
Self::SC0015CredentialTypeSyntaxInvalid => Some("A SCIM CredentialType contained invalid syntax".into()),
Self::SC0016InameSyntaxInvalid => Some("A SCIM Iname string contained invalid syntax".into()),
Self::SC0017Iutf8SyntaxInvalid => Some("A SCIM Iutf8 string contained invalid syntax".into()),
Self::SC0018NsUniqueIdSyntaxInvalid => Some("A SCIM NsUniqueID contained invalid syntax".into()),
Self::SC0019Oauth2ScopeSyntaxInvalid => Some("A SCIM Oauth2 Scope contained invalid syntax".into()),
Self::SC0020Oauth2ScopeMapSyntaxInvalid => Some("A SCIM Oauth2 Scope Map contained invalid syntax".into()),
Self::SC0021Oauth2ScopeMapMissingGroupIdentifier => Some("A SCIM Oauth2 Scope Map was missing a group name or uuid".into()),
Self::SC0022Oauth2ClaimMapSyntaxInvalid => Some("A SCIM Oauth2 Claim Map contained invalid syntax".into()),
Self::SC0023Oauth2ClaimMapMissingGroupIdentifier => Some("A SCIM Claim Map was missing a group name or uuid".into()),
Self::SC0024SshPublicKeySyntaxInvalid => Some("A SCIM Ssh Public Key contained invalid syntax".into()),
Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()),
Self::SC0026Utf8SyntaxInvalid => Some("A SCIM Utf8 String Scope Map contained invalid syntax".into()),
Self::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised.".into()),
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()),
Self::UI0003InvalidOauth2Resume => Some("The server attemped to resume OAuth2, but no OAuth2 session is in progress.".into()),
Self::VL0001ValueSshPublicKeyString => None,
Self::VS0001IncomingReplSshPublicKey => None,
Self::VS0002CertificatePublicKeyDigest |

View file

@ -6,7 +6,9 @@ use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::{Deserialize, Serialize};
use serde_with::base64::{Base64, UrlSafe};
use serde_with::formats::SpaceSeparator;
use serde_with::{formats, serde_as, skip_serializing_none, StringWithSeparator};
use serde_with::{
formats, serde_as, skip_serializing_none, NoneAsEmptyString, StringWithSeparator,
};
use url::Url;
use uuid::Uuid;
@ -38,9 +40,19 @@ pub struct PkceRequest {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthorisationRequest {
// Must be "code". (or token, see 4.2.1)
pub response_type: String,
pub response_type: ResponseType,
/// Response mode.
///
/// Optional; defaults to `query` for `response_type=code` (Auth Code), and
/// `fragment` for `response_type=token` (Implicit Grant, which we probably
/// won't support).
///
/// Reference:
/// [OAuth 2.0 Multiple Response Type Encoding Practices: Response Modes](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes)
pub response_mode: Option<ResponseMode>,
pub client_id: String,
pub state: String,
#[serde_as(as = "NoneAsEmptyString")]
pub state: Option<String>,
#[serde(flatten)]
pub pkce_request: Option<PkceRequest>,
pub redirect_uri: Url,
@ -57,6 +69,39 @@ pub struct AuthorisationRequest {
pub unknown_keys: BTreeMap<String, serde_json::value::Value>,
}
impl AuthorisationRequest {
/// Get the `response_mode` appropriate for this request, taking into
/// account defaults from the `response_type` parameter.
///
/// Returns `None` if the selection is invalid.
///
/// Reference:
/// [OAuth 2.0 Multiple Response Type Encoding Practices: Response Modes](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes)
pub const fn get_response_mode(&self) -> Option<ResponseMode> {
match (self.response_mode, self.response_type) {
// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
// The default Response Mode for this Response Type is the fragment
// encoding and the query encoding MUST NOT be used.
(None, ResponseType::IdToken) => Some(ResponseMode::Fragment),
(Some(ResponseMode::Query), ResponseType::IdToken) => None,
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
(None, ResponseType::Code) => Some(ResponseMode::Query),
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
(None, ResponseType::Token) => Some(ResponseMode::Fragment),
// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
// In no case should a set of Authorization Response parameters
// whose default Response Mode is the fragment encoding be encoded
// using the query encoding.
(Some(ResponseMode::Query), ResponseType::Token) => None,
// Allow others.
(Some(m), _) => Some(m),
}
}
}
/// An OIDC client redirects to the authorisation server with Authorisation Request
/// parameters.
#[skip_serializing_none]
@ -290,15 +335,20 @@ impl AccessTokenIntrospectResponse {
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseType {
// Auth Code flow
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
Code,
// Implicit Grant flow
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
Token,
// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
IdToken,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseMode {
Query,
@ -393,6 +443,21 @@ fn require_request_uri_parameter_supported_default() -> bool {
false
}
#[derive(Serialize, Deserialize, Debug)]
pub struct OidcWebfingerRel {
pub rel: String,
pub href: String,
}
/// The response to an Webfinger request. Only a subset of the body is defined here.
/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct OidcWebfingerResponse {
pub subject: String,
pub links: Vec<OidcWebfingerRel>,
}
/// The response to an OpenID connect discovery request
/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
#[skip_serializing_none]

View file

@ -1,4 +1,5 @@
//! These are types that a client will send to the server.
use super::ScimEntryGetQuery;
use super::ScimOauth2ClaimMapJoinChar;
use crate::attribute::Attribute;
use serde::{Deserialize, Serialize};
@ -89,10 +90,17 @@ pub struct ScimEntryPutKanidm {
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ScimEntryPutGeneric {
// id is only used to target the entry in question
pub id: Uuid,
#[serde(flatten)]
/// Non-standard extension - allow query options to be set in a put request. This
/// is because a put request also returns the entry state post put, so we want
/// to allow putters to adjust and control what is returned here.
pub query: ScimEntryGetQuery,
// external_id can't be set by put
// meta is skipped on put
// Schemas are decoded as part of "attrs".
@ -119,6 +127,10 @@ impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
})
.collect::<Result<_, _>>()?;
Ok(ScimEntryPutGeneric { id, attrs })
Ok(ScimEntryPutGeneric {
id,
attrs,
query: Default::default(),
})
}
}

View file

@ -20,6 +20,7 @@ use crate::attribute::Attribute;
use serde::{Deserialize, Serialize};
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::BTreeMap;
use std::ops::Not;
use utoipa::ToSchema;
use serde_with::formats::CommaSeparator;
@ -47,10 +48,12 @@ pub struct ScimEntryGeneric {
/// SCIM Query Parameters used during the get of a single entry
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ScimEntryGetQuery {
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Attribute>>")]
pub attributes: Option<Vec<Attribute>>,
#[serde(default, skip_serializing_if = "<&bool>::not")]
pub ext_access_check: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
@ -120,12 +123,15 @@ mod tests {
// Group
let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
let group = ScimSyncGroup::builder("testgroup".to_string(), group_uuid)
.set_description(Some("test desc".to_string()))
.set_gidnumber(Some(12345))
.set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
.set_external_id(Some("cn=testgroup".to_string()))
.build();
let group = ScimSyncGroup::builder(
group_uuid,
"cn=testgroup".to_string(),
"testgroup".to_string(),
)
.set_description(Some("test desc".to_string()))
.set_gidnumber(Some(12345))
.set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
.build();
let entry: Result<ScimEntry, _> = group.try_into();
@ -136,32 +142,35 @@ mod tests {
let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
let person =
ScimSyncPerson::builder(user_uuid, "testuser".to_string(), "Test User".to_string())
.set_password_import(Some("new_password".to_string()))
.set_unix_password_import(Some("new_password".to_string()))
.set_totp_import(vec![ScimTotp {
external_id: "Totp".to_string(),
secret: "abcd".to_string(),
algo: "SHA3".to_string(),
step: 60,
digits: 8,
}])
.set_mail(vec![MultiValueAttr {
primary: Some(true),
value: "testuser@example.com".to_string(),
..Default::default()
}])
.set_ssh_publickey(vec![ScimSshPubKey {
label: "Key McKeyface".to_string(),
value: user_sshkey.to_string(),
}])
.set_login_shell(Some("/bin/false".to_string()))
.set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
.set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
.set_gidnumber(Some(54321))
.set_external_id(Some("cn=testuser".to_string()))
.build();
let person = ScimSyncPerson::builder(
user_uuid,
"cn=testuser".to_string(),
"testuser".to_string(),
"Test User".to_string(),
)
.set_password_import(Some("new_password".to_string()))
.set_unix_password_import(Some("new_password".to_string()))
.set_totp_import(vec![ScimTotp {
external_id: "Totp".to_string(),
secret: "abcd".to_string(),
algo: "SHA3".to_string(),
step: 60,
digits: 8,
}])
.set_mail(vec![MultiValueAttr {
primary: Some(true),
value: "testuser@example.com".to_string(),
..Default::default()
}])
.set_ssh_publickey(vec![ScimSshPubKey {
label: "Key McKeyface".to_string(),
value: user_sshkey.to_string(),
}])
.set_login_shell(Some("/bin/false".to_string()))
.set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
.set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
.set_gidnumber(Some(54321))
.build();
let entry: Result<ScimEntry, _> = person.try_into();
@ -172,7 +181,10 @@ mod tests {
fn scim_entry_get_query() {
use super::*;
let q = ScimEntryGetQuery { attributes: None };
let q = ScimEntryGetQuery {
attributes: None,
..Default::default()
};
let txt = serde_urlencoded::to_string(&q).unwrap();
@ -180,6 +192,7 @@ mod tests {
let q = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name]),
ext_access_check: false,
};
let txt = serde_urlencoded::to_string(&q).unwrap();
@ -187,9 +200,10 @@ mod tests {
let q = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name, Attribute::Spn]),
ext_access_check: true,
};
let txt = serde_urlencoded::to_string(&q).unwrap();
assert_eq!(txt, "attributes=name%2Cspn");
assert_eq!(txt, "attributes=name%2Cspn&ext_access_check=true");
}
}

View file

@ -16,14 +16,54 @@ use uuid::Uuid;
/// A strongly typed ScimEntry that is for transmission to clients. This uses
/// Kanidm internal strong types for values allowing direct serialisation and
/// transmission.
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Debug, Clone, ToSchema)]
pub struct ScimEntryKanidm {
#[serde(flatten)]
pub header: ScimEntryHeader,
pub ext_access_check: Option<ScimEffectiveAccess>,
#[serde(flatten)]
pub attrs: BTreeMap<Attribute, ScimValueKanidm>,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
pub enum ScimAttributeEffectiveAccess {
/// All attributes on the entry have this permission granted
Grant,
/// All attributes on the entry have this permission denied
Denied,
/// The following attributes on the entry have this permission granted
Allow(BTreeSet<Attribute>),
}
impl ScimAttributeEffectiveAccess {
/// Check if the effective access allows or denies this attribute
pub fn check(&self, attr: &Attribute) -> bool {
match self {
Self::Grant => true,
Self::Denied => false,
Self::Allow(set) => set.contains(attr),
}
}
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimEffectiveAccess {
/// The identity that inherits the effective permission
pub ident: Uuid,
/// If the ident may delete the target entry
pub delete: bool,
/// The set of effective access over search events
pub search: ScimAttributeEffectiveAccess,
/// The set of effective access over modify present events
pub modify_present: ScimAttributeEffectiveAccess,
/// The set of effective access over modify remove events
pub modify_remove: ScimAttributeEffectiveAccess,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimAddress {

View file

@ -119,7 +119,12 @@ pub struct ScimSyncPersonBuilder {
}
impl ScimSyncPerson {
pub fn builder(id: Uuid, name: String, displayname: String) -> ScimSyncPersonBuilder {
pub fn builder(
id: Uuid,
external_id: String,
name: String,
displayname: String,
) -> ScimSyncPersonBuilder {
ScimSyncPersonBuilder {
inner: ScimSyncPerson {
entry: ScimEntryHeader {
@ -128,7 +133,7 @@ impl ScimSyncPerson {
SCIM_SCHEMA_SYNC_PERSON.to_string(),
],
id,
external_id: None,
external_id: Some(external_id),
meta: None,
},
name,
@ -205,11 +210,6 @@ impl ScimSyncPersonBuilder {
self
}
pub fn set_external_id(mut self, external_id: Option<String>) -> Self {
self.inner.entry.external_id = external_id;
self
}
pub fn build(self) -> ScimSyncPerson {
self.inner
}
@ -220,6 +220,7 @@ pub struct ScimExternalMember {
pub external_id: String,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct ScimSyncGroup {
@ -229,7 +230,8 @@ pub struct ScimSyncGroup {
pub name: String,
pub description: Option<String>,
pub gidnumber: Option<u32>,
pub members: Vec<ScimExternalMember>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub member: Vec<ScimExternalMember>,
}
impl TryInto<ScimEntry> for ScimSyncGroup {
@ -247,19 +249,19 @@ pub struct ScimSyncGroupBuilder {
}
impl ScimSyncGroup {
pub fn builder(name: String, id: Uuid) -> ScimSyncGroupBuilder {
pub fn builder(id: Uuid, external_id: String, name: String) -> ScimSyncGroupBuilder {
ScimSyncGroupBuilder {
inner: ScimSyncGroup {
entry: ScimEntryHeader {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id,
external_id: None,
external_id: Some(external_id),
meta: None,
},
name,
description: None,
gidnumber: None,
members: Vec::with_capacity(0),
member: Vec::with_capacity(0),
},
}
}
@ -288,17 +290,12 @@ impl ScimSyncGroupBuilder {
where
I: Iterator<Item = String>,
{
self.inner.members = member_iter
self.inner.member = member_iter
.map(|external_id| ScimExternalMember { external_id })
.collect();
self
}
pub fn set_external_id(mut self, external_id: Option<String>) -> Self {
self.inner.entry.external_id = external_id;
self
}
pub fn build(self) -> ScimSyncGroup {
self.inner
}

View file

@ -17,7 +17,13 @@ import yarl
from kanidm.models.group import Group, GroupList, IGroup, RawGroup
from kanidm.models.oauth2_rs import IOauth2Rs, OAuth2Rs, Oauth2RsList, RawOAuth2Rs
from kanidm.models.person import IPerson, Person, PersonList, RawPerson
from kanidm.models.person import (
IPerson,
Person,
PersonList,
RawPerson,
PersonCredentialResetToken,
)
from kanidm.models.service_account import (
IServiceAccount,
ServiceAccount,
@ -93,7 +99,7 @@ class KanidmClient:
"""Constructor for KanidmClient"""
self.logger = logger or getLogger(__name__)
self.instance_name = instance_name # TODO: use this in loaders etc
self.instance_name = instance_name # TODO: use this in loaders etc
if config is not None:
self.config = config
else:
@ -123,7 +129,7 @@ class KanidmClient:
def _configure_ssl(self) -> None:
"""Sets up SSL configuration for the client"""
if False in [self.config.verify_certificate, self.config.verify_hostnames ]:
if False in [self.config.verify_certificate, self.config.verify_hostnames]:
logging.debug("Setting up SSL context with no verification")
self._ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
self._ssl_context.hostname_checks_common_name = False
@ -135,9 +141,8 @@ class KanidmClient:
raise FileNotFoundError(f"CA Path not found: {self.config.ca_path}")
else:
self.logger.debug("Setting up SSL context with CA path=%s", self.config.ca_path)
self._ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH,cafile=self.config.ca_path)
self._ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=self.config.ca_path)
else:
logging.debug("Setting up default SSL context")
self._ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
@ -521,9 +526,7 @@ class KanidmClient:
"""get an OAuth2 client"""
endpoint = f"{Endpoints.OAUTH2}/{rs_name}"
response: ClientResponse[IOauth2Rs] = await self.call_get(endpoint)
if response.status_code != 200:
raise ValueError(f"Failed to get oauth2 resource server: {response.content}")
if response.data is None:
if response.status_code != 200 or response.data is None:
raise ValueError(f"Failed to get oauth2 resource server: {response.content}")
return RawOAuth2Rs(**response.data).as_oauth2_rs
@ -583,9 +586,7 @@ class KanidmClient:
"""Get a service account"""
endpoint = f"{Endpoints.SERVICE_ACCOUNT}/{name}"
response: ClientResponse[IServiceAccount] = await self.call_get(endpoint)
if response.status_code != 200:
raise ValueError(f"Failed to get service account: {response.content}")
if response.data is None:
if response.status_code != 200 or response.data is None:
raise ValueError(f"Failed to get service account: {response.content}")
return RawServiceAccount(**response.data).as_service_account
@ -672,9 +673,7 @@ class KanidmClient:
"""Get a group"""
endpoint = f"{Endpoints.GROUP}/{name}"
response: ClientResponse[IGroup] = await self.call_get(endpoint)
if response.status_code != 200:
raise ValueError(f"Failed to get group: {response.content}")
if response.data is None:
if response.status_code != 200 or response.data is None:
raise ValueError(f"Failed to get group: {response.content}")
return RawGroup(**response.data).as_group
@ -719,9 +718,7 @@ class KanidmClient:
"""Get a person by name"""
endpoint = f"{Endpoints.PERSON}/{name}"
response: ClientResponse[IPerson] = await self.call_get(endpoint)
if response.status_code != 200:
raise ValueError(f"Failed to get person: {response.content}")
if response.data is None:
if response.status_code != 200 or response.data is None:
raise ValueError(f"Failed to get person: {response.content}")
return RawPerson(**response.data).as_person
@ -765,6 +762,19 @@ class KanidmClient:
endpoint = f"{Endpoints.PERSON}/{id}"
return await self.call_delete(endpoint)
async def person_account_credential_update_token(self, id: str, ttl: Optional[int] = None) -> PersonCredentialResetToken:
"""Create a password reset token for person with an optional time to live in seconds"""
endpoint = f"{Endpoints.PERSON}/{id}/_credential/_update_intent"
if ttl:
endpoint = f"{endpoint}/{ttl}"
response: ClientResponse[Any] = await self.call_get(endpoint)
if response.status_code != 200 or response.content is None:
raise ValueError(f"Failed to get token: {response.content}")
token = PersonCredentialResetToken.model_validate(json_lib.loads(response.content))
return token
async def person_account_post_ssh_key(self, id: str, tag: str, pubkey: str) -> ClientResponse[None]:
"""Create an SSH key for a user"""
endpoint = f"{Endpoints.PERSON}/{id}/_ssh_pubkeys"

View file

@ -38,8 +38,15 @@ class RawPerson(BaseModel):
uuid=UUID(self.attrs["uuid"][0]),
)
PersonList = RootModel[List[RawPerson]]
class IPerson(TypedDict):
attrs: Dict[str, List[str]]
class PersonCredentialResetToken(BaseModel):
token: str
expiry_time: int
model_config = ConfigDict(arbitrary_types_allowed=True)

View file

@ -1,4 +1,5 @@
""" kanidm RADIUS module """
"""kanidm RADIUS module"""
import asyncio
from aiohttp.client_exceptions import ClientConnectorError
from functools import reduce
@ -16,15 +17,26 @@ from .. import KanidmClient
from . import radiusd
from .utils import check_vlan
CONTAINER_CONFIG_FILE_PATH = "/data/radius.toml"
# the list of places to try
CONFIG_PATHS = [
os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness
"~/.config/kanidm", # for a user
"/etc/kanidm/kanidm", # system-wide
"../examples/kanidm", # test mode
os.getenv("KANIDM_RLM_CONFIG", CONTAINER_CONFIG_FILE_PATH), # container goodness
"~/.config/radius.toml", # for a user
"/etc/kanidm/radius.toml", # system-wide
"../examples/radius.toml", # test mode
"/data/kanidm", # fallback to old path
]
def find_radius_config_path() -> Optional[Path]:
for config_file_path in CONFIG_PATHS:
config_path = Path(config_file_path).expanduser().resolve()
if config_path.exists():
return config_path
return None
def instantiate(_: Any) -> Any:
"""start up radiusd"""
logging.basicConfig(
@ -33,16 +45,9 @@ def instantiate(_: Any) -> Any:
)
logging.info("Starting up!")
config_path = None
for config_file_path in CONFIG_PATHS:
config_path = Path(config_file_path).expanduser().resolve()
if config_path.exists():
break
if (config_path is None) or (not config_path.exists()):
logging.error(
"Failed to find configuration file, checked (%s), quitting!", CONFIG_PATHS
)
config_path = find_radius_config_path()
if config_path is None:
logging.error("Failed to find configuration file, checked (%s), quitting!", CONFIG_PATHS)
sys.exit(1)
kanidm_client = KanidmClient(config_file=config_path)
@ -107,9 +112,7 @@ def authorize(
tok = None
try:
loop = asyncio.get_event_loop()
tok = RadiusTokenResponse.model_validate(
loop.run_until_complete(_get_radius_token(username=user_id))
)
tok = RadiusTokenResponse.model_validate(loop.run_until_complete(_get_radius_token(username=user_id)))
logging.debug("radius information token: %s", tok)
except NoMatchingEntries as error_message:
logging.info(
@ -125,9 +128,7 @@ def authorize(
logging.error("kanidm exception: %s, %s", type(error_message), error_message)
return radiusd.RLM_MODULE_FAIL
if tok is None:
logging.info(
"kanidm RLM_MODULE_REJECT - unable to retrieve radius information token"
)
logging.info("kanidm RLM_MODULE_REJECT - unable to retrieve radius information token")
return radiusd.RLM_MODULE_REJECT
# Get values out of the token

View file

@ -1,4 +1,5 @@
""" type objects """
# pylint: disable=too-few-public-methods
# ^ disabling this because pydantic models don't have public methods
@ -31,7 +32,7 @@ class ClientResponse(BaseModel, Generic[T]):
class AuthInitResponse(BaseModel):
"""Aelps parse the response from the Auth 'init' stage"""
"""Helps parse the response from the Auth 'init' stage"""
class _AuthInitState(BaseModel):
"""sub-class for the AuthInitResponse model"""
@ -146,9 +147,7 @@ class RadiusClient(BaseModel):
socket.gethostbyname(value)
return value
except socket.gaierror as error:
raise ValueError(
f"ipaddr value ({value}) wasn't an IP Address, Network or valid hostname: {error}"
)
raise ValueError(f"ipaddr value ({value}) wasn't an IP Address, Network or valid hostname: {error}")
class KanidmClientConfig(BaseModel):
@ -172,7 +171,6 @@ class KanidmClientConfig(BaseModel):
radius_cert_path: str = "/data/cert.pem"
radius_key_path: str = "/data/key.pem" # the signing key for radius TLS
radius_dh_path: str = "/data/dh.pem" # the diffie-hellman output
radius_ca_path: Optional[str] = None
radius_ca_dir: Optional[str] = None
@ -196,9 +194,7 @@ class KanidmClientConfig(BaseModel):
uri = urlparse(value)
valid_schemes = ["http", "https"]
if uri.scheme not in valid_schemes:
raise ValueError(
f"Invalid URL Scheme for uri='{value}': '{uri.scheme}' - expected one of {valid_schemes}"
)
raise ValueError(f"Invalid URL Scheme for uri='{value}': '{uri.scheme}' - expected one of {valid_schemes}")
# make sure the URI ends with a /
if not value.endswith("/"):

View file

@ -1,4 +1,4 @@
""" utility functions """
"""utility functions"""
from pathlib import Path
from typing import Any, Dict, Union

761
pykanidm/poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@ -13,87 +13,92 @@ files = [
[[package]]
name = "aiohttp"
version = "3.11.8"
version = "3.11.12"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
files = [
{file = "aiohttp-3.11.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2ca685c6a851ce64e511fbcb906e4dd97d13e567ca7ecb5cb30b184e15dc6d"},
{file = "aiohttp-3.11.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52913bb8a0a72a57479f54b281300c9d23036aa9aa3ebbc9a32a643484eadfc2"},
{file = "aiohttp-3.11.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35dafc70051b6cbd6dafb533b4e3f0df6225a4896be373ef86367b2987409331"},
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:561b9596a9f90266673ef0b950c27e04ab597cdb53785e2ac91b83b33c31b509"},
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d479c1fdcc920056a06d04059db52eb8590ecbbb3acdcaeeea26a88ff782e94a"},
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ce8eb6444bb6e862feca664ce365afa8e2e32db24dcf1a502719a8a002f9274"},
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9bf08eb93611b1d4d6245b6fecf88728e90eece00e00d554e1b0c445557d83"},
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a20ddaa58fea717177fac9a4a1fb8b39be868aa4fed2af6de4313b7a08f0f71"},
{file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9f4aadfea6b48cfa17aef1a68ba6bee5a0246374f5a588e299a4f4ff5bd1c77b"},
{file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa7deebb4bc5143745e6282139d7b9de50beb6d06609df64d2c993ef496bc7eb"},
{file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fe503a76b9e3a13b62e64545693c9463afe9d429e0909120f7bb66de91ed8bc2"},
{file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1c5838a68e31712354129add1b5fe32b06aa05275f835130edc650e6288af05f"},
{file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:832e58d9454fe501b0d092cdf660c0e34e16005f61acd06e1c79b0fc45019c94"},
{file = "aiohttp-3.11.8-cp310-cp310-win32.whl", hash = "sha256:00618c37a350884c08e87cf9a6532be274d564227ac49e0b474cf41f27e1f190"},
{file = "aiohttp-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:8eeaac75203da1a54afe1faea3c855a1973026b54929112aa9b67bceadbcb0ca"},
{file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8dd02b44555893adfe7cc4b3b454fee04f9dcec45cf66ef5bb53ebf393f0505"},
{file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658052941324edea3dee1f681375e70779f55e437e07bdfc4b5bbe65ad53cefb"},
{file = "aiohttp-3.11.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c829471a9e2266da4a0666f8a9e215f19320f79778af379c1c7db324ac24ed2"},
{file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21951756690f5d86d0215da38eb0fd65def03b5e2a1c08a4a39718a6d0d48f2"},
{file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fa50ddc6b21cc1ae23e13524d6f75b27e279fdf5cf905b2df6fd171891ac4e2"},
{file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5afbd805e449048ecebb1a256176e953d4ca9e48bab387d4d1c8524f1c7a95"},
{file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea68db69f2a4ddc24b28b8e754fc0b963ed7f9b9a76137f06fe44643d6821fbd"},
{file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b3ac163145660ce660aed2f1005e6d4de840d39728990b7250525eeec4e4a8"},
{file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9ac0cce897904b77e109e5403ed713187dbdf96832bfd061ac07164264be16c"},
{file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3260c77cff4e35245bc517658bd54d7a64787f71f3c4f723877c82f22835b032"},
{file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7fd9c11ffad6b022bf02a41a70418cb2ab3b33f2c27842a5999e3ab78daf280"},
{file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16bda233a7b159ab08107e8858fedca90a9de287057fab54cafde51bd83f9819"},
{file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4867008617bbf86e9fb5b00f72dd0e3a00a579b32233caff834320867f9b7cac"},
{file = "aiohttp-3.11.8-cp311-cp311-win32.whl", hash = "sha256:17e6b9d8e29e3bfc7f893f327e92c9769d3582cee2fb1652c1431ac3f60115a0"},
{file = "aiohttp-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:7f3be4961a5c2c670f31caab7641a37ea2a97031f0d8ae15bcfd36b6bf273200"},
{file = "aiohttp-3.11.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e3b5bfef913d6be270c81976fbc0cbf66625cd92663bbb7e03b3adbd6aa4ac6"},
{file = "aiohttp-3.11.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb51a81cb637b9a072c9cfae1839e35c6579638861eb3479eb5d6e6ce8bc6782"},
{file = "aiohttp-3.11.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd2ca84e5f7a35f313a62eb7d6a50bac6760b60bafce34586750712731c0aeff"},
{file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47c6663df9446aa848b478413219600da4b54bc0409e1ac4bc80fb1a81501363"},
{file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c665ed4b52256614858b20711bbbd2755b0e19ec86870f8ff1645acf9ae9e760"},
{file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35d4545e7684da7a954ffc2dce495462cb16a902dffdebe98572408f6aaaee83"},
{file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85be3899e6860dd2cd3f4370ded6708e939d00d5ec922a8eb328d114db605a47"},
{file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ed9f1f2697713c48efc9ec483ad5d062e4aa91854f090a3eba0b19c002851d"},
{file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c0dbae99737badf3f5e862088a118e28d3b36f03eb608a6382eddfd68178e05b"},
{file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:beae08f900b2980af4353a0200eb162b39f276fd8a6e43079a540f83964671f4"},
{file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d6f9e5fd1b3ecbaca3e04a15a02d1fa213248608caee99fd5bdddd4759959cf7"},
{file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7def89a41fe32120d89cd4577f5efbab3c52234c5890066ced8a2f7202dff88"},
{file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98f596cf59292e779bc387f22378a3d2c5e052c9fe2bf822ac4f547c6fe57758"},
{file = "aiohttp-3.11.8-cp312-cp312-win32.whl", hash = "sha256:b64fa6b76b35b695cd3e5c42a4e568cbea8d41c9e59165e2a43da00976e2027e"},
{file = "aiohttp-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:afba47981ff73b1794c00dce774334dcfe62664b3b4f78f278b77d21ce9daf43"},
{file = "aiohttp-3.11.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a81525430da5ca356fae6e889daeb6f5cc0d5f0cef88e59cdde48e2394ea1365"},
{file = "aiohttp-3.11.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7565689e86a88c1d258351ebd14e343337b76a56ca5c0a2c1db96ec28149386f"},
{file = "aiohttp-3.11.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0f9dbe9763c014c408ad51a027dc9582518e992dc63e2ffe359ac1b4840a560"},
{file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca580edc3ccd7f6ea76ad9cf59f5a8756d338e770b5eda7be26bcda8fa7ef53"},
{file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d141631a7348038fc7b5d1a81b3c9afa9aa056188ded7902fe754028fdea5c5"},
{file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64e6b14608a56a4c76c60daac730b0c0eeaf9d10dfc3231f7fc26521a0d628fd"},
{file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0983d0ce329f2f9dbeb355c3744bd6333f34e0dc56025b6b7d4f285b90acb51e"},
{file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d96b93a46a3742880fa21bcb35c6c40cf27714ec0fb8ec85fe444d73b95131b9"},
{file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4f1779c3142d913c509c2ed1de8b8f920e07a5cd65ac1f57c61cfb6bfded5a4"},
{file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:48be7cff468c9c0d86a02e6a826e1fe159094b16d5aa2c17703e7317f791b0f9"},
{file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:daea456b79ca2bacc7f062845bbb1139c3b3231fc83169da5a682cf385416dd1"},
{file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c92e763cf641e10ad9342597d20060ba23de5e411aada96660e679e3f9371189"},
{file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a750ee5a177e0f873d6b2d7d0fa6e1e7c658fc0ca8ea56438dcba2ac94bedb09"},
{file = "aiohttp-3.11.8-cp313-cp313-win32.whl", hash = "sha256:4448c9c7f77bad48a6569062c0c16deb77fbb7363de1dc71ed087f66fb3b3c96"},
{file = "aiohttp-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:481075a1949de79a8a6841e0086f2f5f464785c592cf527ed0db2c0cbd0e1ba2"},
{file = "aiohttp-3.11.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:72779bfb34d6d6b51e55a7f4901b410e416b5431738b367d49696928c91a2ca8"},
{file = "aiohttp-3.11.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e6523f39071a01757048985e4cc22d04aa130bc40d9128503f3a61a3ee98328"},
{file = "aiohttp-3.11.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:220bbce18b3046973465be45415430f1cab39d7fdc40cbcf0a8c05485c6902fe"},
{file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336bbf7a33dd8cb4a7afb98c70e9935a81e5e88f7ac595ba2e84b1fb5da190d6"},
{file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c5e4f1ba5059b85e05c551961a448ce2689c6249ed6a2e2174796842c191d10"},
{file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9f9fd5c672c962389429abd11ed32c9c93f7932fd58584cae1e43951b141c6b"},
{file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58bd94ad48143e1d42e05fc055da41de0a9933f378ad87760595b8aec83d317b"},
{file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bf52642b12d70d78c18882915201bc5345f7c8f0f2ab8919d99b886aa6475a7"},
{file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fee12d8487b0df2b683424cca2a0d8fb7281d5607518d742e98119a74af01026"},
{file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:65fd04f1fea668ad1af48ac31b752000e222dccffedcad3de8ccf9d34489ccd3"},
{file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c3f397e0511a0ec4fe331e602fc057dfd336d352062deb9969ebd81e253a149c"},
{file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cf8f05f4abe3288fe2e106e1461fd20d8abf6103886ddfb6d746a5b8fb830d2b"},
{file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7d71d4ac0792ff89541179394d303be846a0b6cd3821ae67286ee69ecec16f9f"},
{file = "aiohttp-3.11.8-cp39-cp39-win32.whl", hash = "sha256:2b6f8716044ae5e5f2a3b4e4b6bfee48e97c8b2a92e56f43aadd728c7fd26b7d"},
{file = "aiohttp-3.11.8-cp39-cp39-win_amd64.whl", hash = "sha256:da343903214bf9f9d314b913caa499fa19e26d73e6e23a3db7d4898ea6d47028"},
{file = "aiohttp-3.11.8.tar.gz", hash = "sha256:7bc9d64a2350cbb29a9732334e1a0743cbb6844de1731cbdf5949b235653f3fd"},
{file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"},
{file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"},
{file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"},
{file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"},
{file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"},
{file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"},
{file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"},
{file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"},
{file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"},
{file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"},
{file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"},
{file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"},
{file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"},
{file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"},
{file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"},
{file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"},
{file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"},
{file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"},
{file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"},
{file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"},
{file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"},
{file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"},
{file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"},
{file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"},
{file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"},
{file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"},
{file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"},
{file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"},
{file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"},
{file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"},
{file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"},
{file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"},
{file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"},
{file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"},
{file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"},
{file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"},
{file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"},
{file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"},
{file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"},
{file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"},
{file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"},
{file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"},
{file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"},
{file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"},
{file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"},
{file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"},
{file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"},
{file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"},
{file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"},
{file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"},
{file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"},
{file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"},
{file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"},
{file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"},
{file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"},
{file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"},
]
[package.dependencies]
@ -180,13 +185,13 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]]
name = "authlib"
version = "1.3.2"
version = "1.4.1"
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"},
{file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"},
{file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"},
{file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"},
]
[package.dependencies]
@ -206,52 +211,6 @@ files = [
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
version = "24.10.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
files = [
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
{file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
{file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
{file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
{file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2024.8.30"
@ -468,73 +427,68 @@ files = [
[[package]]
name = "coverage"
version = "7.6.8"
version = "7.6.11"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"},
{file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"},
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"},
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"},
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"},
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"},
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"},
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"},
{file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"},
{file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"},
{file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"},
{file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"},
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"},
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"},
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"},
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"},
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"},
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"},
{file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"},
{file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"},
{file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"},
{file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"},
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"},
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"},
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"},
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"},
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"},
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"},
{file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"},
{file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"},
{file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"},
{file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"},
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"},
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"},
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"},
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"},
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"},
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"},
{file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"},
{file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"},
{file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"},
{file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"},
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"},
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"},
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"},
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"},
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"},
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"},
{file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"},
{file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"},
{file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"},
{file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"},
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"},
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"},
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"},
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"},
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"},
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"},
{file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"},
{file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"},
{file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"},
{file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"},
{file = "coverage-7.6.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd"},
{file = "coverage-7.6.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3"},
{file = "coverage-7.6.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306"},
{file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c"},
{file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70"},
{file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e"},
{file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff"},
{file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47"},
{file = "coverage-7.6.11-cp310-cp310-win32.whl", hash = "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0"},
{file = "coverage-7.6.11-cp310-cp310-win_amd64.whl", hash = "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da"},
{file = "coverage-7.6.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840"},
{file = "coverage-7.6.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec"},
{file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50"},
{file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b"},
{file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27"},
{file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2"},
{file = "coverage-7.6.11-cp311-cp311-win32.whl", hash = "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b"},
{file = "coverage-7.6.11-cp311-cp311-win_amd64.whl", hash = "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7"},
{file = "coverage-7.6.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36"},
{file = "coverage-7.6.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea"},
{file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce"},
{file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297"},
{file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963"},
{file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce"},
{file = "coverage-7.6.11-cp312-cp312-win32.whl", hash = "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12"},
{file = "coverage-7.6.11-cp312-cp312-win_amd64.whl", hash = "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551"},
{file = "coverage-7.6.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251"},
{file = "coverage-7.6.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67"},
{file = "coverage-7.6.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4"},
{file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06"},
{file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f"},
{file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd"},
{file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d"},
{file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c"},
{file = "coverage-7.6.11-cp313-cp313-win32.whl", hash = "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4"},
{file = "coverage-7.6.11-cp313-cp313-win_amd64.whl", hash = "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2"},
{file = "coverage-7.6.11-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5"},
{file = "coverage-7.6.11-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f"},
{file = "coverage-7.6.11-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b"},
{file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652"},
{file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19"},
{file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"},
{file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd"},
{file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7"},
{file = "coverage-7.6.11-cp313-cp313t-win32.whl", hash = "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0"},
{file = "coverage-7.6.11-cp313-cp313t-win_amd64.whl", hash = "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c"},
{file = "coverage-7.6.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521"},
{file = "coverage-7.6.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449"},
{file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda"},
{file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9"},
{file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce"},
{file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e"},
{file = "coverage-7.6.11-cp39-cp39-win32.whl", hash = "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03"},
{file = "coverage-7.6.11-cp39-cp39-win_amd64.whl", hash = "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a"},
{file = "coverage-7.6.11-pp39.pp310-none-any.whl", hash = "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b"},
{file = "coverage-7.6.11-py3-none-any.whl", hash = "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7"},
{file = "coverage-7.6.11.tar.gz", hash = "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286"},
]
[package.extras]
@ -807,13 +761,13 @@ colors = ["colorama (>=0.4.6)"]
[[package]]
name = "jinja2"
version = "3.1.4"
version = "3.1.5"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
]
[package.dependencies]
@ -999,13 +953,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
[[package]]
name = "mkdocs-autorefs"
version = "1.2.0"
version = "1.3.0"
description = "Automatically link across pages in MkDocs."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"},
{file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"},
{file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"},
{file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"},
]
[package.dependencies]
@ -1032,13 +986,13 @@ pyyaml = ">=5.1"
[[package]]
name = "mkdocs-material"
version = "9.5.47"
version = "9.6.3"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2"},
{file = "mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede"},
{file = "mkdocs_material-9.6.3-py3-none-any.whl", hash = "sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf"},
{file = "mkdocs_material-9.6.3.tar.gz", hash = "sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832"},
]
[package.dependencies]
@ -1055,7 +1009,7 @@ regex = ">=2022.4"
requests = ">=2.26,<3.0"
[package.extras]
git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
@ -1072,24 +1026,23 @@ files = [
[[package]]
name = "mkdocstrings"
version = "0.27.0"
version = "0.28.0"
description = "Automatic documentation from sources, for MkDocs."
optional = false
python-versions = ">=3.9"
files = [
{file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"},
{file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"},
{file = "mkdocstrings-0.28.0-py3-none-any.whl", hash = "sha256:84cf3dc910614781fe0fee46ce8006fde7df6cc7cca2e3f799895fb8a9170b39"},
{file = "mkdocstrings-0.28.0.tar.gz", hash = "sha256:df20afef1eafe36ba466ae20732509ecb74237653a585f5061937e54b553b4e0"},
]
[package.dependencies]
click = ">=7.0"
importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.11.1"
Markdown = ">=3.6"
MarkupSafe = ">=1.1"
mkdocs = ">=1.4"
mkdocs-autorefs = ">=1.2"
platformdirs = ">=2.2"
mkdocs-autorefs = ">=1.3"
mkdocs-get-deps = ">=0.2"
pymdown-extensions = ">=6.3"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""}
@ -1100,19 +1053,20 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "1.12.2"
version = "1.14.6"
description = "A Python handler for mkdocstrings."
optional = false
python-versions = ">=3.9"
files = [
{file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"},
{file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"},
{file = "mkdocstrings_python-1.14.6-py3-none-any.whl", hash = "sha256:e0ca11b49ac0f23070afb566245f4ff80ea1700e03c9dbc143d70dbf1cae074e"},
{file = "mkdocstrings_python-1.14.6.tar.gz", hash = "sha256:3fb6589491614422d781dacca085c0c5a53a7063af37a2fea5864e24e378b03e"},
]
[package.dependencies]
griffe = ">=0.49"
mkdocs-autorefs = ">=1.2"
mkdocstrings = ">=0.26"
mkdocstrings = ">=0.28"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[[package]]
name = "multidict"
@ -1215,49 +1169,49 @@ files = [
[[package]]
name = "mypy"
version = "1.13.0"
version = "1.15.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
{file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
{file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
{file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
{file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
{file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
mypy_extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.6.0"
typing_extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
@ -1361,13 +1315,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pook"
version = "2.1.2"
version = "2.1.3"
description = "HTTP traffic mocking and expectations made easy"
optional = false
python-versions = ">=3.9"
files = [
{file = "pook-2.1.2-py3-none-any.whl", hash = "sha256:ea76784ee1440ee8dde08047c6a1f68be46a07d07a5180d068a047db5620ca1d"},
{file = "pook-2.1.2.tar.gz", hash = "sha256:4acbb9d13ac18b807fd3a54b414a22a16b75db6e5048bc88461479da03c4ecbf"},
{file = "pook-2.1.3-py3-none-any.whl", hash = "sha256:f8e75e2e41b1f6da37d0bc6b77a0f4da33c4d4de382105046efd644fe5ca2f8e"},
{file = "pook-2.1.3.tar.gz", hash = "sha256:441191c0f3d014b141ca71430a0c2bfa6d2369ac24703a3fdfbbf5a25146d8c0"},
]
[package.dependencies]
@ -1495,18 +1449,18 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.2"
version = "2.10.6"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
{file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.27.1"
pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2"
[package.extras]
@ -1515,111 +1469,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
version = "2.27.1"
version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
]
[package.dependencies]
@ -1685,12 +1639,12 @@ pylint = ">=1.7"
[[package]]
name = "pylint-pydantic"
version = "0.3.2"
version = "0.3.5"
description = "A Pylint plugin to help Pylint understand the Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pylint_pydantic-0.3.2-py3-none-any.whl", hash = "sha256:e5cec02370aa68ac8eff138e5d573b0ac049bab864e9a6c3a9057cf043440aa1"},
{file = "pylint_pydantic-0.3.5-py3-none-any.whl", hash = "sha256:e7a54f09843b000676633ed02d5985a4a61c8da2560a3b0d46082d2ff171c4a1"},
]
[package.dependencies]
@ -1698,21 +1652,6 @@ pydantic = "<3.0"
pylint = ">2.0,<4.0"
pylint-plugin-utils = "*"
[[package]]
name = "pylint-pytest"
version = "1.1.7"
description = "A Pylint plugin to suppress pytest-related false positives."
optional = false
python-versions = ">=3.6"
files = [
{file = "pylint-pytest-1.1.7.tar.gz", hash = "sha256:7a38be02c014eb6d98791eb978e79ed292f1904d3a518289c6d7ac4fb4122e98"},
{file = "pylint_pytest-1.1.7-py3-none-any.whl", hash = "sha256:5d687a2f4b17e85654fc2a8f04944761efb11cb15dc46d008f420c377b149151"},
]
[package.dependencies]
pylint = ">=2"
pytest = ">=4.6"
[[package]]
name = "pymdown-extensions"
version = "10.9"
@ -1755,39 +1694,39 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-aiohttp"
version = "1.0.5"
version = "1.1.0"
description = "Pytest plugin for aiohttp support"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
files = [
{file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"},
{file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"},
{file = "pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d"},
{file = "pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc"},
]
[package.dependencies]
aiohttp = ">=3.8.1"
aiohttp = ">=3.11.0b0"
pytest = ">=6.1.0"
pytest-asyncio = ">=0.17.2"
[package.extras]
testing = ["coverage (==6.2)", "mypy (==0.931)"]
testing = ["coverage (==6.2)", "mypy (==1.12.1)"]
[[package]]
name = "pytest-asyncio"
version = "0.24.0"
version = "0.25.3"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
{file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
@ -2135,29 +2074,29 @@ files = [
[[package]]
name = "ruff"
version = "0.8.1"
version = "0.9.5"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"},
{file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"},
{file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"},
{file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"},
{file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"},
{file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"},
{file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"},
{file = "ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442"},
{file = "ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a"},
{file = "ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393"},
{file = "ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2"},
{file = "ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee"},
{file = "ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1"},
{file = "ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a"},
{file = "ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5"},
{file = "ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723"},
{file = "ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6"},
{file = "ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9"},
{file = "ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c"},
]
[[package]]
@ -2204,6 +2143,20 @@ files = [
{file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
]
[[package]]
name = "types-requests"
version = "2.32.0.20241016"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"},
{file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"},
]
[package.dependencies]
urllib3 = ">=2"
[[package]]
name = "types-toml"
version = "0.10.8.20240310"
@ -2414,4 +2367,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "d5fc77baf896f8ff41baa174c20f9070bb9dba6b38ba47c64f667d47272eae76"
content-hash = "f614f512e5cfcf28eb72369010b689568f2736563e498bd6cf252292eb8bf08c"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "kanidm"
version = "1.0.0"
version = "1.0.1"
description = "Kanidm client library"
license = "MPL-2.0"
@ -27,25 +27,22 @@ pydantic = ">=2.0.0,<3.0.0"
aiohttp = "^3.8.1"
Authlib = "^1.2.0"
[tool.poetry.dev-dependencies]
mypy = "^1.13"
pytest = "^8.3.4"
types-toml = "^0.10.8"
pylint-pydantic = "^0.3.2"
coverage = "^7.6.8"
pylint-pytest = "^1.1.7"
pytest-asyncio = "^0.24.0"
pytest-mock = "^3.14.0"
pytest-aiohttp = "^1.0.5"
black = "^24.10.0"
mkdocs = "^1.5.3"
mkdocs-material = "^9.5.47"
mkdocstrings = "^0.27.0"
mkdocstrings-python = "^1.12.2"
pook = "^2.1.2"
[tool.poetry.group.dev.dependencies]
ruff = ">=0.5.1,<0.8.2"
ruff = ">=0.5.1,<0.9.6"
pytest = "^8.3.4"
mypy = "^1.14.1"
types-requests = "^2.32.0.20241016"
pytest-aiohttp = "^1.1.0"
pytest-mock = "^3.14.0"
types-toml = "^0.10.8.20240310"
pylint-pydantic = "^0.3.5"
coverage = "^7.6.10"
mkdocs = "^1.6.1"
mkdocs-material = "^9.6.1"
mkdocstrings = ">=0.27,<0.29"
mkdocstrings-python = "^1.13.0"
pook = "^2.1.3"
[build-system]
requires = ["poetry-core>=1.0.0"]

View file

@ -63,7 +63,7 @@ RUN python3 -m pip install \
COPY rlm_python/radius_entrypoint.py /radius_entrypoint.py
RUN mkdir /data && chown radiusd /data
RUN chmod a+r /etc/raddb/certs/ -R
USER $RADIUS_USER

View file

@ -75,23 +75,21 @@ eap {
#
# EAP-pwd -- secure password-based authentication
#
# pwd {
# group = 19
#pwd {
# group = 19
# server_id = theserver@example.com
#
# This has the same meaning as for TLS.
# fragment_size = 1020
#
# server_id = theserver@example.com
# This has the same meaning as for TLS.
# fragment_size = 1020
# The virtual server which determines the
# "known good" password for the user.
# Note that unlike TLS, only the "authorize"
# section is processed. EAP-PWD requests can be
# distinguished by having a User-Name, but
# no User-Password, CHAP-Password, EAP-Message, etc.
# virtual_server = "inner-tunnel"
# }
# The virtual server which determines the
# "known good" password for the user.
# Note that unlike TLS, only the "authorize"
# section is processed. EAP-PWD requests can be
# distinguished by having a User-Name, but
# no User-Password, CHAP-Password, EAP-Message, etc.
# virtual_server = "inner-tunnel"
# }
# Cisco LEAP
#
@ -236,7 +234,7 @@ eap {
#
# openssl dhparam -out certs/dh 2048
#
dh_file = ${certdir}/dh.pem
#dh_file = ${certdir}/dh.pem
#
# If your system doesn't have /dev/urandom,

View file

@ -10,6 +10,7 @@ import sys
from typing import Any
# import toml
import kanidm.radius
from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config
@ -17,8 +18,6 @@ DEBUG = True
if os.environ.get('DEBUG', False):
DEBUG = True
CONFIG_FILE_PATH = "/data/kanidm"
CERT_SERVER_DEST = "/etc/raddb/certs/server.pem"
CERT_CA_DEST = "/etc/raddb/certs/ca.pem"
CERT_CA_DIR = "/etc/raddb/certs/"
@ -59,7 +58,11 @@ def setup_certs(
sys.exit(1)
if cert_ca != CERT_CA_DEST:
print(f"Copying {cert_ca} to {CERT_CA_DEST}")
shutil.copyfile(cert_ca, CERT_CA_DEST)
try:
shutil.copyfile(cert_ca, CERT_CA_DEST)
except shutil.SameFileError:
pass
# This dir can also contain crls!
if kanidm_config_object.radius_ca_dir:
@ -75,19 +78,6 @@ def setup_certs(
# not hashed as a ca.
subprocess.check_call(["openssl", "rehash", CERT_CA_DIR])
# let's put some dhparams in place
if kanidm_config_object.radius_dh_path is not None:
cert_dh = Path(kanidm_config_object.radius_dh_path).expanduser().resolve()
if not cert_dh.exists():
# print(f"Failed to find radiusd dh file ({cert_dh}), quitting!", file=sys.stderr)
# sys.exit(1)
print(f"Generating dh params in {cert_dh}")
subprocess.check_call(["openssl", "dhparam", "-out", cert_dh, "2048"])
if cert_dh != CERT_DH_DEST:
print(f"Copying {cert_dh} to {CERT_DH_DEST}")
shutil.copyfile(cert_dh, CERT_DH_DEST)
server_key = Path(kanidm_config_object.radius_key_path).expanduser().resolve()
if not server_key.exists() or not server_key.is_file():
print(
@ -157,15 +147,15 @@ def run_radiusd() -> None:
if __name__ == '__main__':
signal.signal(signal.SIGCHLD, _sigchild_handler)
config_file = Path(CONFIG_FILE_PATH).expanduser().resolve()
if not config_file.exists:
config_file = kanidm.radius.find_radius_config_path()
if config_file is None:
print(
"Failed to find configuration file ({config_file}), quitting!",
file=sys.stderr,
)
sys.exit(1)
kanidm_config = KanidmClientConfig.parse_obj(load_config(CONFIG_FILE_PATH))
kanidm_config = KanidmClientConfig.model_validate(load_config(config_file))
setup_certs(kanidm_config)
write_clients_conf(kanidm_config)
print("Configuration set up, starting...")

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

View file

@ -18,6 +18,7 @@ sudo apt-get install -y \
jq \
libpam0g-dev \
libssl-dev \
libsystemd-dev \
libudev-dev \
pkg-config \
ripgrep
@ -43,4 +44,4 @@ cargo install mdbook-alerts --version 0.6.4
cargo install deno --locked
echo "Done!"
echo "Done!"

View file

@ -13,6 +13,7 @@ ${SUDOCMD} apt-get update &&
libpam0g-dev \
libudev-dev \
libssl-dev \
libsystemd-dev \
pkg-config \
curl \
rsync \

View file

@ -1,64 +0,0 @@
#!/bin/bash
set -e
if [ "$(rustup default | grep -cE '^nightly' )" -eq 0 ]; then
echo "You need to switch to rust nightly!"
exit 1
fi
# if [ "$(which rustfilt | wc -l )" -eq 0 ]; then
# echo "You need to have rustfilt on the path"
# echo "cargo install rustfilt"
# exit 1
# fi
if [ "$(which llvm-cov | wc -l )" -eq 0 ]; then
echo "You need to have llvm-cov on the path"
exit 1
fi
export CARGO_INCREMENTAL=0
export LLVM_PROFILE_FILE
echo "Profile files going into ${LLVM_PROFILE_FILE}"
echo "Running tests"
#shellcheck disable=SC2068
LLVM_PROFILE_FILE="$(pwd)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test
grcov . --binary-path ./target/debug/deps/ \
-s . \
-t html \
--branch \
--ignore-not-existing \
--ignore '../*' \
--ignore "/*" \
-o target/coverage/html
# PROFDATA="./target/profile/kanidm.profdata"
# llvm-profdata merge ./target/profile/*.profraw -o "${PROFDATA}"
# llvm-cov report --ignore-filename-regex="\.cargo" \
# --enable-name-compression \
# $( \
# for file in \
# $( \
# RUSTFLAGS="-C instrument-coverage" \
# cargo test --tests --no-run --message-format=json \
# | jq -r "select(.profile.test == true) | .filenames[]" \
# | grep -v dSYM - \
# ); \
# do \
# printf "%s %s " -object $file; \
# done \
# ) \
# --instr-profile="${PROFDATA}" --summary-only
# llvm-cov show -Xdemangler=rustfilt target/debug/kanidmd \
# -instr-profile="${PROFDATA}" \
# -show-line-counts-or-regions \
# -show-instantiations \
# -name-regex="kani.*"

View file

@ -29,6 +29,7 @@ RUN \
libopenssl-3-devel \
pam-devel \
sqlite3-devel \
systemd-devel \
rsync \
findutils \
which \

View file

@ -25,7 +25,7 @@ askama_axum = { workspace = true }
axum = { workspace = true }
axum-htmx = { workspace = true }
axum-extra = { version = "0.9.6", features = ["cookie"] }
axum-macros = "0.4.1"
axum-macros = "0.4.2"
axum-server = { version = "0.7.1", default-features = false }
bytes = { workspace = true }
chrono = { workspace = true }
@ -45,19 +45,20 @@ ldap3_proto = { workspace = true }
libc = { workspace = true }
openssl = { workspace = true }
opentelemetry = { workspace = true, features = ["logs"] }
# opentelemetry_api = { workspace = true, features = ["logs"] }
qrcode = { workspace = true, features = ["svg"] }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true }
sketching = { workspace = true }
sshkeys = { workspace = true }
sshkey-attest = { workspace = true }
time = { workspace = true, features = ["serde", "std", "local-offset"] }
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
tokio-openssl = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
toml = { workspace = true }
tower = { version = "0.5.1", features = ["tokio-stream", "tracing"] }
tower = { version = "0.5.2", features = ["tokio-stream", "tracing"] }
tower-http = { version = "0.6.2", features = [
"compression-gzip",
"fs",
@ -88,6 +89,14 @@ webauthn-rs = { workspace = true, features = [
[dev-dependencies]
walkdir = { workspace = true }
tempfile = { workspace = true }
kanidmd_lib = { workspace = true, features = ["test"] }
[build-dependencies]
kanidm_build_profiles = { workspace = true }
[package.metadata.cargo-machete]
ignored = [
"opentelemetry", # feature gated
"kanidm_build_profiles",
]

View file

@ -0,0 +1,16 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
languageOptions: {
globals: {
...globals.browser,
Base64 : "writeable" // to feed the Base64 class into the global scope
}
}
},
pluginJs.configs.recommended,
];

1104
server/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
server/core/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "kanidm",
"version": "0.0.1-dev",
"description": "Kanidm UI Javascript - not a publishable package, just for development",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/kanidm/kanidm.git"
},
"author": "",
"license": "MPL-2.0",
"bugs": {
"url": "https://github.com/kanidm/kanidm/issues"
},
"homepage": "https://kanidm.com",
"devDependencies": {
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"globals": "^15.14.0",
"prettier": "^3.4.2"
},
"scripts" : {
"prettier": "npx prettier --ignore-path 'prettier-ignore' ./static --check",
"prettier:fix": "npx prettier --ignore-path 'prettier-ignore' ./static --write"
}
}

View file

@ -0,0 +1 @@
static/external/

View file

@ -9,6 +9,7 @@ use kanidm_proto::internal::{
IdentifyUserRequest, IdentifyUserResponse, ImageValue, OperationError, RadiusAuthToken,
SearchRequest, SearchResponse, UserAuthToken,
};
use kanidm_proto::oauth2::OidcWebfingerResponse;
use kanidm_proto::v1::{
AuthIssueSession, AuthRequest, Entry as ProtoEntry, UatStatus, UnixGroupToken, UnixUserToken,
WhoamiResponse,
@ -37,7 +38,7 @@ use kanidmd_lib::{
idm::ldap::{LdapBoundToken, LdapResponseState},
idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AuthorisationRequest,
AuthoriseResponse, JwkKeySet, Oauth2Error, Oauth2Rfc8414MetadataResponse,
AuthoriseReject, AuthoriseResponse, JwkKeySet, Oauth2Error, Oauth2Rfc8414MetadataResponse,
OidcDiscoveryResponse, OidcToken,
},
idm::server::{DomainInfoRead, IdmServerTransaction},
@ -1441,7 +1442,7 @@ impl QueryServerReadV1 {
client_auth_info: ClientAuthInfo,
consent_req: String,
eventid: Uuid,
) -> Result<Url, OperationError> {
) -> Result<AuthoriseReject, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await?;
let ident = idms_prox_read
@ -1509,6 +1510,21 @@ impl QueryServerReadV1 {
idms_prox_read.oauth2_openid_discovery(&client_id)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_webfinger_discovery(
&self,
client_id: &str,
resource_id: &str,
eventid: Uuid,
) -> Result<OidcWebfingerResponse, OperationError> {
let mut idms_prox_read = self.idms.proxy_read().await?;
idms_prox_read.oauth2_openid_webfinger(client_id, resource_id)
}
#[instrument(
level = "info",
skip_all,

View file

@ -250,31 +250,28 @@ impl ServerConfig {
/// Updates the ServerConfig from environment variables starting with `KANIDM_`
fn try_from_env(mut self) -> Result<Self, String> {
for (key, value) in std::env::vars() {
if !key.starts_with("KANIDM_") {
let Some(key) = key.strip_prefix("KANIDM_") else {
continue;
}
};
let ignorable_build_fields = [
"KANIDM_CPU_FLAGS",
"KANIDM_CPU_FLAGS",
"KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_HTMX_UI_PKG_PATH",
"KANIDM_PKG_VERSION_HASH",
"KANIDM_PKG_VERSION",
"KANIDM_PRE_RELEASE",
"KANIDM_PROFILE_NAME",
"CPU_FLAGS",
"DEFAULT_CONFIG_PATH",
"DEFAULT_UNIX_SHELL_PATH",
"HTMX_UI_PKG_PATH",
"PKG_VERSION",
"PKG_VERSION_HASH",
"PRE_RELEASE",
"PROFILE_NAME",
];
if ignorable_build_fields.contains(&key.as_str()) {
if ignorable_build_fields.contains(&key) {
#[cfg(any(debug_assertions, test))]
eprintln!("-- Ignoring build-time env var {}", key);
eprintln!("-- Ignoring build-time env var KANIDM_{key}");
continue;
}
match key.replace("KANIDM_", "").as_str() {
match key {
"DOMAIN" => {
self.domain = Some(value.to_string());
}
@ -414,7 +411,7 @@ impl ServerConfig {
self.otel_grpc_url = Some(value.to_string());
}
_ => eprintln!("Ignoring env var {}", key),
_ => eprintln!("Ignoring env var KANIDM_{key}"),
}
}
@ -574,7 +571,7 @@ impl Configuration {
Configuration {
address: DEFAULT_SERVER_ADDRESS.to_string(),
ldapaddress: None,
adminbindpath: env!("KANIDM_ADMIN_BIND_PATH").to_string(),
adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(),
threads: std::thread::available_parallelism()
.map(|t| t.get())
.unwrap_or_else(|_e| {

View file

@ -1,10 +1,11 @@
use axum::extract::State;
use axum::http::header::CONTENT_TYPE;
use axum::response::IntoResponse;
use axum::response::{IntoResponse, Redirect};
use axum::{Extension, Json};
use kanidmd_lib::status::StatusRequestEvent;
use super::middleware::KOpId;
use super::views::constants::Urls;
use super::ServerState;
#[utoipa::path(
@ -50,3 +51,15 @@ pub async fn robots_txt() -> impl IntoResponse {
),
)
}
#[utoipa::path(
get,
path = Urls::WellKnownChangePassword.as_ref(),
responses(
(status = 303, description = "See other"),
),
tag = "ui",
)]
pub async fn redirect_to_update_credentials() -> impl IntoResponse {
Redirect::to(Urls::UpdateCredentials.as_ref())
}

View file

@ -53,6 +53,7 @@ use tokio::{
use tokio_openssl::SslStream;
use tower::Service;
use tower_http::{services::ServeDir, trace::TraceLayer};
use url::Url;
use uuid::Uuid;
use std::io::ErrorKind;
@ -62,16 +63,17 @@ use std::{net::SocketAddr, str::FromStr};
#[derive(Clone)]
pub struct ServerState {
pub status_ref: &'static StatusActor,
pub qe_w_ref: &'static QueryServerWriteV1,
pub qe_r_ref: &'static QueryServerReadV1,
pub(crate) status_ref: &'static StatusActor,
pub(crate) qe_w_ref: &'static QueryServerWriteV1,
pub(crate) qe_r_ref: &'static QueryServerReadV1,
// Store the token management parts.
pub jws_signer: JwsHs256Signer,
pub(crate) jws_signer: JwsHs256Signer,
pub(crate) trust_x_forward_for: bool,
pub csp_header: HeaderValue,
pub domain: String,
pub(crate) csp_header: HeaderValue,
pub(crate) origin: Url,
pub(crate) domain: String,
// This is set to true by default, and is only false on integration tests.
pub secure_cookies: bool,
pub(crate) secure_cookies: bool,
}
impl ServerState {
@ -129,7 +131,7 @@ pub(crate) fn get_js_files(role: ServerRole) -> Result<Vec<JavaScriptFile>, ()>
if !matches!(role, ServerRole::WriteReplicaNoUI) {
// let's set up the list of js module hashes
let pkg_path = env!("KANIDM_HTMX_UI_PKG_PATH").to_owned();
let pkg_path = env!("KANIDM_SERVER_UI_PKG_PATH").to_owned();
let filelist = [
"external/bootstrap.bundle.min.js",
@ -138,11 +140,13 @@ pub(crate) fn get_js_files(role: ServerRole) -> Result<Vec<JavaScriptFile>, ()>
"external/base64.js",
"modules/cred_update.mjs",
"pkhtml.js",
"style.js",
];
for filepath in filelist {
match generate_integrity_hash(format!("{}/{}", pkg_path, filepath,)) {
Ok(hash) => {
debug!("Integrity hash for {}: {}", filepath, hash);
let js = JavaScriptFile { hash };
all_pages.push(js)
}
@ -209,6 +213,12 @@ pub async fn create_https_server(
let trust_x_forward_for = config.trust_x_forward_for;
let origin = Url::parse(&config.origin)
// Should be impossible!
.map_err(|err| {
error!(?err, "Unable to parse origin URL - refusing to start. You must correct the value for origin. {:?}", config.origin);
})?;
let state = ServerState {
status_ref,
qe_w_ref,
@ -216,6 +226,7 @@ pub async fn create_https_server(
jws_signer,
trust_x_forward_for,
csp_header,
origin,
domain: config.domain.clone(),
secure_cookies: config.integration_test_config.is_none(),
};
@ -240,16 +251,20 @@ pub async fn create_https_server(
.merge(oauth2::route_setup(state.clone()))
.merge(v1_scim::route_setup())
.merge(v1::route_setup(state.clone()))
.route("/robots.txt", get(generic::robots_txt));
.route("/robots.txt", get(generic::robots_txt))
.route(
views::constants::Urls::WellKnownChangePassword.as_ref(),
get(generic::redirect_to_update_credentials),
);
let app = match config.role {
ServerRole::WriteReplicaNoUI => app,
ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => {
let pkg_path = PathBuf::from(env!("KANIDM_HTMX_UI_PKG_PATH"));
let pkg_path = PathBuf::from(env!("KANIDM_SERVER_UI_PKG_PATH"));
if !pkg_path.exists() {
eprintln!(
"Couldn't find htmx UI package path: ({}), quitting.",
env!("KANIDM_HTMX_UI_PKG_PATH")
env!("KANIDM_SERVER_UI_PKG_PATH")
);
std::process::exit(1);
}

View file

@ -20,7 +20,6 @@ use axum::{
Extension, Form, Json, Router,
};
use axum_macros::debug_handler;
use compact_jwt::{JwkKeySet, OidcToken};
use kanidm_proto::constants::uri::{
OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT, OAUTH2_AUTHORISE_REJECT,
};
@ -30,8 +29,8 @@ use kanidm_proto::oauth2::AuthorisationResponse;
#[cfg(feature = "dev-oauth2-device-flow")]
use kanidm_proto::oauth2::DeviceAuthorizationResponse;
use kanidmd_lib::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthoriseResponse,
ErrorResponse, Oauth2Error, TokenRevokeRequest,
};
use kanidmd_lib::prelude::f_eq;
use kanidmd_lib::prelude::*;
@ -258,22 +257,14 @@ async fn oauth2_authorise(
.body(body.into())
.unwrap()
}
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
})) => {
Ok(AuthoriseResponse::Permitted(success)) => {
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
// We could consider changing this to 303?
#[allow(clippy::unwrap_used)]
let body =
Body::from(serde_json::to_string(&AuthorisationResponse::Permitted).unwrap());
let redirect_uri = success.build_redirect_uri();
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::FOUND)
@ -378,18 +369,11 @@ async fn oauth2_authorise_permit(
.await;
match res {
Ok(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
}) => {
Ok(success) => {
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
// We could consider changing this to 303?
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
let redirect_uri = success.build_redirect_uri();
#[allow(clippy::expect_used)]
Response::builder()
.status(StatusCode::FOUND)
@ -464,12 +448,9 @@ async fn oauth2_authorise_reject(
.await;
match res {
Ok(mut redirect_uri) => {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("error", "access_denied")
.append_pair("error_description", "authorisation rejected");
Ok(reject) => {
let redirect_uri = reject.build_redirect_uri();
#[allow(clippy::unwrap_used)]
Response::builder()
.header(LOCATION, redirect_uri.as_str())
@ -532,7 +513,7 @@ pub async fn oauth2_token_post(
}
}
// // For future openid integration
// For future openid integration
pub async fn oauth2_openid_discovery_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
@ -557,6 +538,46 @@ pub async fn oauth2_openid_discovery_get(
}
}
#[derive(Deserialize)]
pub struct Oauth2OpenIdWebfingerQuery {
resource: String,
}
pub async fn oauth2_openid_webfinger_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
Query(query): Query<Oauth2OpenIdWebfingerQuery>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let Oauth2OpenIdWebfingerQuery { resource } = query;
let cleaned_resource = resource.strip_prefix("acct:").unwrap_or(&resource);
let res = state
.qe_r_ref
.handle_oauth2_webfinger_discovery(&client_id, cleaned_resource, kopid.eventid)
.await;
match res {
Ok(mut dsc) => (
StatusCode::OK,
[
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
(CONTENT_TYPE, "application/jrd+json"),
],
Json({
dsc.subject = resource;
dsc
}),
)
.into_response(),
Err(e) => {
error!(err = ?e, "Unable to access discovery info");
WebError::from(e).response_with_access_control_origin_header()
}
}
}
pub async fn oauth2_rfc8414_metadata_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
@ -587,13 +608,13 @@ pub async fn oauth2_openid_userinfo_get(
Path(client_id): Path<String>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
) -> Result<Json<OidcToken>, HTTPOauth2Error> {
) -> Response {
// The token we want to inspect is in the authorisation header.
let client_token = match client_auth_info.bearer_token {
Some(val) => val,
None => {
error!("Bearer Authentication Not Provided");
return Err(HTTPOauth2Error(Oauth2Error::AuthenticationRequired));
return HTTPOauth2Error(Oauth2Error::AuthenticationRequired).into_response();
}
};
@ -603,8 +624,13 @@ pub async fn oauth2_openid_userinfo_get(
.await;
match res {
Ok(uir) => Ok(Json(uir)),
Err(e) => Err(HTTPOauth2Error(e)),
Ok(uir) => (
StatusCode::OK,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
Json(uir),
)
.into_response(),
Err(e) => HTTPOauth2Error(e).into_response(),
}
}
@ -612,13 +638,18 @@ pub async fn oauth2_openid_publickey_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> Result<Json<JwkKeySet>, WebError> {
state
) -> Response {
let res = state
.qe_r_ref
.handle_oauth2_openid_publickey(client_id, kopid.eventid)
.await
.map(Json::from)
.map_err(WebError::from)
.map_err(WebError::from);
match res {
Ok(jsn) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], jsn).into_response(),
Err(web_err) => web_err.response_with_access_control_origin_header(),
}
}
/// This is called directly by the resource server, where we then issue
@ -779,17 +810,23 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
"/oauth2/openid/:client_id/.well-known/openid-configuration",
get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
)
.route(
"/oauth2/openid/:client_id/.well-known/webfinger",
get(oauth2_openid_webfinger_get).options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/openid/:client_id/userinfo",
get(oauth2_openid_userinfo_get).options(oauth2_preflight_options),
get(oauth2_openid_userinfo_get)
.post(oauth2_openid_userinfo_get)
.options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/openid/:client_id/public_key.jwk",
get(oauth2_openid_publickey_get),
get(oauth2_openid_publickey_get).options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OAUTH2 DISCOVERY URLS

View file

@ -200,6 +200,7 @@ pub(crate) async fn oauth2_id_scopemap_post(
Json(scopes): Json<Vec<String>>,
) -> Result<Json<()>, WebError> {
let filter = oauth2_id(&rs_name);
state
.qe_w_ref
.handle_oauth2_scopemap_update(client_auth_info, group, scopes, filter, kopid.eventid)

View file

@ -7,8 +7,8 @@ use super::v1::{
};
use super::ServerState;
use crate::https::extractors::VerifiedClientInformation;
use axum::extract::{Path, Query, State};
use axum::response::Html;
use axum::extract::{rejection::JsonRejection, DefaultBodyLimit, Path, Query, State};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use kanidm_proto::scim_v1::{
@ -17,6 +17,8 @@ use kanidm_proto::scim_v1::{
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidmd_lib::prelude::*;
const DEFAULT_SCIM_SYNC_BYTES: usize = 1024 * 1024 * 32;
#[utoipa::path(
get,
path = "/v1/sync_account",
@ -271,14 +273,25 @@ async fn scim_sync_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Json(changes): Json<ScimSyncRequest>,
) -> Result<Json<()>, WebError> {
state
.qe_w_ref
.handle_scim_sync_apply(client_auth_info, changes, kopid.eventid)
.await
.map(Json::from)
.map_err(WebError::from)
payload: Result<Json<ScimSyncRequest>, JsonRejection>,
) -> Response {
match payload {
Ok(Json(changes)) => {
let res = state
.qe_w_ref
.handle_scim_sync_apply(client_auth_info, changes, kopid.eventid)
.await;
match res {
Ok(data) => Json::from(data).into_response(),
Err(err) => WebError::from(err).into_response(),
}
}
Err(rejection) => {
error!(?rejection, "Unable to process JSON");
rejection.into_response()
}
}
}
#[utoipa::path(
@ -473,6 +486,11 @@ pub fn route_setup() -> Router<ServerState> {
//
// POST Send a sync update
//
.route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get))
.route(
"/scim/v1/Sync",
post(scim_sync_post)
.layer(DefaultBodyLimit::max(DEFAULT_SCIM_SYNC_BYTES))
.get(scim_sync_get),
)
.route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
}

View file

@ -43,7 +43,7 @@ pub(crate) async fn view_apps_get(
.qe_r_ref
.handle_list_applinks(client_auth_info, kopid.eventid)
.await
.map_err(|old| HtmxError::new(&kopid, old))?;
.map_err(|old| HtmxError::new(&kopid, old, domain_info.clone()))?;
Ok({
(

View file

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
pub(crate) enum ProfileMenuItems {
UserProfile,
Credentials,
EnrolDevice,
UnixPassword,
}
@ -20,15 +21,16 @@ impl std::fmt::Display for UiMessage {
}
}
#[allow(dead_code)]
pub(crate) enum Urls {
Apps,
CredReset,
EnrolDevice,
Profile,
UpdateCredentials,
Oauth2Resume,
Login,
Ui,
WellKnownChangePassword,
}
impl AsRef<str> for Urls {
@ -36,11 +38,13 @@ impl AsRef<str> for Urls {
match self {
Self::Apps => "/ui/apps",
Self::CredReset => "/ui/reset",
Self::EnrolDevice => "/ui/enrol",
Self::Profile => "/ui/profile",
Self::UpdateCredentials => "/ui/update_credentials",
Self::Oauth2Resume => "/ui/oauth2/resume",
Self::Login => "/ui/login",
Self::Ui => "/ui",
Self::WellKnownChangePassword => "/.well-known/change-password",
}
}
}

View file

@ -6,27 +6,7 @@ use compact_jwt::{Jws, JwsSigner};
use serde::de::DeserializeOwned;
use serde::Serialize;
pub fn destroy(jar: CookieJar, ck_id: &str) -> CookieJar {
if let Some(ck) = jar.get(ck_id) {
let mut ck = ck.clone();
ck.make_removal();
/*
if let Some(path) = ck.path().cloned() {
ck.set_path(&path);
}
*/
jar.add(ck)
} else {
jar
}
}
pub fn make_unsigned<'a>(
state: &'_ ServerState,
ck_id: &'a str,
value: String,
path: &'a str,
) -> Cookie<'a> {
fn new_cookie<'a>(state: &'_ ServerState, ck_id: &'a str, value: String) -> Cookie<'a> {
let mut token_cookie = Cookie::new(ck_id, value);
token_cookie.set_secure(state.secure_cookies);
token_cookie.set_same_site(SameSite::Lax);
@ -36,17 +16,37 @@ pub fn make_unsigned<'a>(
// of the idm to share the cookie. If domain was incorrect
// then webauthn won't work anyway!
token_cookie.set_domain(state.domain.clone());
token_cookie.set_path(path);
// These last forever.
token_cookie.make_permanent();
token_cookie.set_path("/");
token_cookie
}
#[instrument(name = "views::cookies::destroy", level = "debug", skip(jar, state))]
pub fn destroy(jar: CookieJar, ck_id: &str, state: &ServerState) -> CookieJar {
if let Some(ck) = jar.get(ck_id) {
let mut removal_cookie = ck.clone();
removal_cookie.make_removal();
// Need to be set to domain else the cookie isn't removed!
removal_cookie.set_domain(state.domain.clone());
// Need to be set to / to remove on all parent paths.
// If you don't set a path, NOTHING IS REMOVED!!!
removal_cookie.set_path("/");
jar.add(removal_cookie)
} else {
jar
}
}
pub fn make_unsigned<'a>(state: &'_ ServerState, ck_id: &'a str, value: String) -> Cookie<'a> {
new_cookie(state, ck_id, value)
}
pub fn make_signed<'a, T: Serialize>(
state: &'_ ServerState,
ck_id: &'a str,
value: &'_ T,
path: &'a str,
) -> Option<Cookie<'a>> {
let kref = &state.jws_signer;
@ -65,15 +65,7 @@ pub fn make_signed<'a, T: Serialize>(
})
.ok()?;
let mut token_cookie = Cookie::new(ck_id, token);
token_cookie.set_secure(state.secure_cookies);
token_cookie.set_same_site(SameSite::Lax);
token_cookie.set_http_only(true);
token_cookie.set_path(path);
token_cookie.set_domain(state.domain.clone());
// These last forever, we have our own internal expiration handling.
token_cookie.make_permanent();
Some(token_cookie)
Some(new_cookie(state, ck_id, token))
}
pub fn get_signed<T: DeserializeOwned>(

View file

@ -0,0 +1,116 @@
use askama::Template;
use askama_axum::IntoResponse;
use axum::extract::State;
use axum::response::Response;
use axum::Extension;
use axum_extra::extract::CookieJar;
use kanidm_proto::internal::UserAuthToken;
use qrcode::render::svg;
use qrcode::QrCode;
use url::Url;
use std::time::Duration;
use super::constants::Urls;
use super::navbar::NavbarCtx;
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
use crate::https::middleware::KOpId;
use crate::https::views::constants::ProfileMenuItems;
use crate::https::views::errors::HtmxError;
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
use crate::https::ServerState;
#[derive(Template)]
#[template(path = "user_settings.html")]
struct ProfileView {
navbar_ctx: NavbarCtx,
profile_partial: EnrolDeviceView,
}
#[derive(Template)]
#[template(path = "enrol_device.html")]
pub(crate) struct EnrolDeviceView {
menu_active_item: ProfileMenuItems,
secret: String,
qr_code_svg: String,
uri: Url,
}
pub(crate) async fn view_enrol_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar,
) -> axum::response::Result<Response> {
let uat: UserAuthToken = state
.qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.await
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
let can_rw = uat.purpose_readwrite_active(time);
// The user lacks an elevated session, request a re-auth.
if !can_rw {
let display_ctx = LoginDisplayCtx {
domain_info,
oauth2: None,
reauth: Some(Reauth {
username: uat.spn,
purpose: ReauthPurpose::ProfileSettings,
}),
error: None,
};
return Ok(super::login::view_reauth_get(
state,
client_auth_info,
kopid,
jar,
Urls::EnrolDevice.as_ref(),
display_ctx,
)
.await);
}
let cu_intent = state
.qe_w_ref
.handle_idmcredentialupdateintent(
client_auth_info,
uat.spn,
Some(Duration::from_secs(900)),
kopid.eventid,
)
.await
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
let secret = cu_intent.token;
let mut uri = state.origin.clone();
uri.set_path(Urls::CredReset.as_ref());
uri.set_query(Some(format!("token={secret}").as_str()));
let qr_code_svg = match QrCode::new(uri.as_str()) {
Ok(qr) => qr.render::<svg::Color>().build(),
Err(qr_err) => {
error!("Failed to create TOTP QR code: {qr_err}");
"QR Code Generation Failed".to_string()
}
};
Ok(ProfileView {
navbar_ctx: NavbarCtx { domain_info },
profile_partial: EnrolDeviceView {
menu_active_item: ProfileMenuItems::EnrolDevice,
qr_code_svg,
secret,
uri,
},
}
.into_response())
}

View file

@ -1,6 +1,7 @@
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response};
use axum_htmx::{HxReswap, HxRetarget, SwapOption};
use kanidmd_lib::idm::server::DomainInfoRead;
use utoipa::ToSchema;
use uuid::Uuid;
@ -21,19 +22,19 @@ use crate::https::views::UnrecoverableErrorView;
#[derive(Debug, ToSchema)]
pub(crate) enum HtmxError {
/// Something went wrong when doing things.
OperationError(Uuid, OperationError),
OperationError(Uuid, OperationError, DomainInfoRead),
}
impl HtmxError {
pub(crate) fn new(kopid: &KOpId, operr: OperationError) -> Self {
HtmxError::OperationError(kopid.eventid, operr)
pub(crate) fn new(kopid: &KOpId, operr: OperationError, domain_info: DomainInfoRead) -> Self {
HtmxError::OperationError(kopid.eventid, operr, domain_info)
}
}
impl IntoResponse for HtmxError {
fn into_response(self) -> Response {
match self {
HtmxError::OperationError(kopid, inner) => {
HtmxError::OperationError(kopid, inner, domain_info) => {
let body = serde_json::to_string(&inner).unwrap_or(inner.to_string());
match &inner {
OperationError::NotAuthenticated
@ -58,6 +59,7 @@ impl IntoResponse for HtmxError {
UnrecoverableErrorView {
err_code: inner,
operation_id: kopid,
domain_info,
},
)
.into_response(),

View file

@ -12,9 +12,10 @@ use axum::{
response::{IntoResponse, Redirect, Response},
Extension, Form, Json,
};
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use axum_extra::extract::cookie::{CookieJar, SameSite};
use kanidm_proto::internal::{
COOKIE_AUTH_SESSION_ID, COOKIE_BEARER_TOKEN, COOKIE_OAUTH2_REQ, COOKIE_USERNAME,
COOKIE_AUTH_SESSION_ID, COOKIE_BEARER_TOKEN, COOKIE_CU_SESSION_TOKEN, COOKIE_OAUTH2_REQ,
COOKIE_USERNAME,
};
use kanidm_proto::v1::{
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthStep,
@ -47,6 +48,7 @@ struct SessionContext {
after_auth_loc: Option<String>,
}
#[derive(Clone)]
pub enum ReauthPurpose {
ProfileSettings,
}
@ -58,7 +60,7 @@ impl fmt::Display for ReauthPurpose {
}
}
}
#[derive(Clone)]
pub enum LoginError {
InvalidUsername,
}
@ -70,16 +72,17 @@ impl fmt::Display for LoginError {
}
}
}
#[derive(Clone)]
pub struct Reauth {
pub username: String,
pub purpose: ReauthPurpose,
}
#[derive(Clone)]
pub struct Oauth2Ctx {
pub client_name: String,
}
#[derive(Clone)]
pub struct LoginDisplayCtx {
pub domain_info: DomainInfoRead,
// We only need this on the first re-auth screen to indicate what we are doing
@ -159,9 +162,10 @@ pub async fn view_logout_get(
State(state): State<ServerState>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Extension(kopid): Extension<KOpId>,
DomainInfo(domain_info): DomainInfo,
mut jar: CookieJar,
) -> Response {
if let Err(err_code) = state
let response = if let Err(err_code) = state
.qe_w_ref
.handle_logout(client_auth_info, kopid.eventid)
.await
@ -169,15 +173,20 @@ pub async fn view_logout_get(
UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response()
} else {
let response = Redirect::to(Urls::Login.as_ref()).into_response();
Redirect::to(Urls::Login.as_ref()).into_response()
};
jar = cookies::destroy(jar, COOKIE_BEARER_TOKEN);
// Always clear cookies even on an error.
jar = cookies::destroy(jar, COOKIE_BEARER_TOKEN, &state);
jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
jar = cookies::destroy(jar, COOKIE_AUTH_SESSION_ID, &state);
jar = cookies::destroy(jar, COOKIE_CU_SESSION_TOKEN, &state);
(jar, response).into_response()
}
(jar, response).into_response()
}
pub async fn view_reauth_get(
@ -190,14 +199,7 @@ pub async fn view_reauth_get(
) -> Response {
// No matter what, we always clear the stored oauth2 cookie to prevent
// ui loops
let jar = if let Some(authreq_cookie) = jar.get(COOKIE_OAUTH2_REQ) {
let mut authreq_cookie = authreq_cookie.clone();
authreq_cookie.make_removal();
authreq_cookie.set_path(Urls::Ui.as_ref());
jar.add(authreq_cookie)
} else {
jar
};
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
let session_valid_result = state
.qe_r_ref
@ -234,7 +236,7 @@ pub async fn view_reauth_get(
ar,
client_auth_info,
session_context,
display_ctx,
display_ctx.clone(),
)
.await
{
@ -243,6 +245,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info: display_ctx.clone().domain_info,
}
.into_response(),
}
@ -251,6 +254,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
}
.into_response(),
}
@ -277,6 +281,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
}
.into_response(),
}
@ -324,14 +329,7 @@ pub async fn view_index_get(
// No matter what, we always clear the stored oauth2 cookie to prevent
// ui loops
let jar = if let Some(authreq_cookie) = jar.get(COOKIE_OAUTH2_REQ) {
let mut authreq_cookie = authreq_cookie.clone();
authreq_cookie.make_removal();
authreq_cookie.set_path(Urls::Ui.as_ref());
jar.add(authreq_cookie)
} else {
jar
};
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
match session_valid_result {
Ok(()) => {
@ -367,6 +365,7 @@ pub async fn view_index_get(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
}
@ -429,7 +428,7 @@ pub async fn view_login_begin_post(
};
let mut display_ctx = LoginDisplayCtx {
domain_info,
domain_info: domain_info.clone(),
oauth2: None,
reauth: None,
error: None,
@ -454,6 +453,7 @@ pub async fn view_login_begin_post(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
}
@ -472,6 +472,7 @@ pub async fn view_login_begin_post(
_ => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
},
@ -512,7 +513,7 @@ pub async fn view_login_mech_choose_post(
.await;
let display_ctx = LoginDisplayCtx {
domain_info,
domain_info: domain_info.clone(),
oauth2: None,
reauth: None,
error: None,
@ -537,6 +538,7 @@ pub async fn view_login_mech_choose_post(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
}
@ -545,6 +547,7 @@ pub async fn view_login_mech_choose_post(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
}
@ -552,6 +555,8 @@ pub async fn view_login_mech_choose_post(
#[derive(Debug, Clone, Deserialize)]
pub struct LoginTotpForm {
#[serde(default, deserialize_with = "empty_string_as_none")]
password: Option<String>,
totp: String,
}
@ -560,7 +565,7 @@ pub async fn view_login_totp_post(
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar,
mut jar: CookieJar,
Form(login_totp_form): Form<LoginTotpForm>,
) -> Response {
// trim leading and trailing white space.
@ -583,6 +588,31 @@ pub async fn view_login_totp_post(
}
};
// In some flows the PW manager may not have autocompleted the pw until
// this point. This could be due to a re-auth flow which skips the username
// prompt, the use of remember-me+return which then skips the autocomplete.
//
// In the case the pw *is* bg filled, we need to add it to the session context
// here.
//
// It's probably not "optimal" to be getting the context out and signing it
// here to re-add it, but it also helps keep the flow neater in general.
if let Some(password_autofill) = login_totp_form.password {
let mut session_context =
cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID)
.unwrap_or_default();
session_context.password = Some(password_autofill);
// If we can't write this back to the jar, we warn and move on.
if let Ok(update_jar) = add_session_cookie(&state, jar.clone(), &session_context) {
jar = update_jar;
} else {
warn!("Unable to update session_context, ignoring...");
}
}
let auth_cred = AuthCredential::Totp(totp);
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
}
@ -644,7 +674,7 @@ pub async fn view_login_passkey_post(
}
Err(e) => {
error!(err = ?e, "Unable to deserialize credential submission");
HtmxError::new(&kopid, OperationError::SerdeJsonError).into_response()
HtmxError::new(&kopid, OperationError::SerdeJsonError, domain_info).into_response()
}
}
}
@ -674,7 +704,7 @@ async fn credential_step(
.unwrap_or_default();
let display_ctx = LoginDisplayCtx {
domain_info,
domain_info: domain_info.clone(),
oauth2: None,
reauth: None,
error: None,
@ -702,7 +732,7 @@ async fn credential_step(
ar,
client_auth_info,
session_context,
display_ctx,
display_ctx.clone(),
)
.await
{
@ -711,6 +741,7 @@ async fn credential_step(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
}
.into_response(),
}
@ -719,6 +750,7 @@ async fn credential_step(
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
}
.into_response(),
}
@ -767,6 +799,7 @@ async fn view_login_step(
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
}
.into_response()
}
@ -817,10 +850,8 @@ async fn view_login_step(
break res;
}
AuthState::Continue(allowed) => {
// Reauth inits its session here so we need to be able to add cookie here ig.
if jar.get(COOKIE_AUTH_SESSION_ID).is_none() {
jar = add_session_cookie(&state, jar, &session_context)?;
}
// Reauth inits its session here so we need to be able to add it's cookie here.
jar = add_session_cookie(&state, jar, &session_context)?;
let res = match allowed.len() {
// Shouldn't be possible.
@ -829,6 +860,7 @@ async fn view_login_step(
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
}
.into_response()
}
@ -897,32 +929,30 @@ async fn view_login_step(
// Update jar
let token_str = token.to_string();
// Important - this can be make unsigned as token_str has it's own
// Important - this can be make unsigned as token_str has its own
// signatures.
let bearer_cookie = cookies::make_unsigned(
&state,
COOKIE_BEARER_TOKEN,
token_str.clone(),
"/",
);
let mut bearer_cookie =
cookies::make_unsigned(&state, COOKIE_BEARER_TOKEN, token_str.clone());
// Important - can be permanent as the token has its own expiration time internally
bearer_cookie.make_permanent();
jar = if session_context.remember_me {
// Important - can be unsigned as username is just for remember
// me and no other purpose.
let username_cookie = cookies::make_unsigned(
let mut username_cookie = cookies::make_unsigned(
&state,
COOKIE_USERNAME,
session_context.username.clone(),
Urls::Login.as_ref(),
);
username_cookie.make_permanent();
jar.add(username_cookie)
} else {
jar
};
jar = jar
.add(bearer_cookie)
.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
jar = jar.add(bearer_cookie);
jar = cookies::destroy(jar, COOKIE_AUTH_SESSION_ID, &state);
// Now, we need to decided where to go.
let res = if jar.get(COOKIE_OAUTH2_REQ).is_some() {
@ -939,7 +969,7 @@ async fn view_login_step(
}
AuthState::Denied(reason) => {
debug!("🧩 -> AuthState::Denied");
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
jar = cookies::destroy(jar, COOKIE_AUTH_SESSION_ID, &state);
break LoginDeniedView {
display_ctx,
@ -959,16 +989,11 @@ fn add_session_cookie(
jar: CookieJar,
session_context: &SessionContext,
) -> Result<CookieJar, OperationError> {
cookies::make_signed(
state,
COOKIE_AUTH_SESSION_ID,
session_context,
Urls::Login.as_ref(),
)
.map(|mut cookie| {
// Not needed when redirecting into this site
cookie.set_same_site(SameSite::Strict);
jar.add(cookie)
})
.ok_or(OperationError::InvalidSessionState)
cookies::make_signed(state, COOKIE_AUTH_SESSION_ID, session_context)
.map(|mut cookie| {
// Not needed when redirecting into this site
cookie.set_same_site(SameSite::Strict);
jar.add(cookie)
})
.ok_or(OperationError::InvalidSessionState)
}

View file

@ -9,13 +9,17 @@ use axum::{
use axum_htmx::HxRequestGuardLayer;
use constants::Urls;
use kanidmd_lib::prelude::{OperationError, Uuid};
use kanidmd_lib::{
idm::server::DomainInfoRead,
prelude::{OperationError, Uuid},
};
use crate::https::ServerState;
mod apps;
mod constants;
pub(crate) mod constants;
mod cookies;
mod enrol;
mod errors;
mod login;
mod navbar;
@ -28,6 +32,8 @@ mod reset;
struct UnrecoverableErrorView {
err_code: OperationError,
operation_id: Uuid,
// This is an option because it's not always present in an "unrecoverable" situation
domain_info: DomainInfoRead,
}
pub fn view_router() -> Router<ServerState> {
@ -37,6 +43,7 @@ pub fn view_router() -> Router<ServerState> {
get(|| async { Redirect::permanent(Urls::Login.as_ref()) }),
)
.route("/apps", get(apps::view_apps_get))
.route("/enrol", get(enrol::view_enrol_get))
.route("/reset", get(reset::view_reset_get))
.route("/update_credentials", get(reset::view_self_reset_get))
.route("/profile", get(profile::view_profile_get))
@ -96,6 +103,10 @@ pub fn view_router() -> Router<ServerState> {
.route("/reset/change_password", post(reset::view_new_pwd))
.route("/reset/add_passkey", post(reset::view_new_passkey))
.route("/reset/set_unixcred", post(reset::view_set_unixcred))
.route(
"/reset/add_ssh_publickey",
post(reset::view_add_ssh_publickey),
)
.route("/api/delete_alt_creds", post(reset::remove_alt_creds))
.route("/api/delete_unixcred", post(reset::remove_unixcred))
.route("/api/add_totp", post(reset::add_totp))
@ -103,6 +114,10 @@ pub fn view_router() -> Router<ServerState> {
.route("/api/remove_passkey", post(reset::remove_passkey))
.route("/api/finish_passkey", post(reset::finish_passkey))
.route("/api/cancel_mfareg", post(reset::cancel_mfareg))
.route(
"/api/remove_ssh_publickey",
post(reset::remove_ssh_publickey),
)
.route("/api/cu_cancel", post(reset::cancel_cred_update))
.route("/api/cu_commit", post(reset::commit))
.layer(HxRequestGuardLayer::new("/ui"));
@ -128,3 +143,29 @@ where
.map(Some),
}
}
#[cfg(test)]
mod tests {
use askama_axum::IntoResponse;
use super::*;
#[tokio::test]
async fn test_unrecoverableerrorview() {
let domain_info = kanidmd_lib::server::DomainInfo::new_test();
let view = UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: Uuid::new_v4(),
domain_info: domain_info.read(),
};
let error_html = view.render().expect("Failed to render");
assert!(error_html.contains(domain_info.read().display_name()));
let response = view.into_response();
// TODO: this really should be an error code :(
assert_eq!(response.status(), 200);
}
}

View file

@ -3,9 +3,7 @@ use crate::https::{
middleware::KOpId,
ServerState,
};
use kanidmd_lib::idm::oauth2::{
AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse, Oauth2Error,
};
use kanidmd_lib::idm::oauth2::{AuthorisationRequest, AuthoriseResponse, Oauth2Error};
use kanidmd_lib::prelude::*;
use kanidm_proto::internal::COOKIE_OAUTH2_REQ;
@ -26,7 +24,6 @@ use axum_extra::extract::cookie::{CookieJar, SameSite};
use axum_htmx::HX_REDIRECT;
use serde::Deserialize;
use super::constants::Urls;
use super::login::{LoginDisplayCtx, Oauth2Ctx};
use super::{cookies, UnrecoverableErrorView};
@ -96,14 +93,7 @@ async fn oauth2_auth_req(
) -> Response {
// No matter what, we always clear the stored oauth2 cookie to prevent
// ui loops
let jar = if let Some(authreq_cookie) = jar.get(COOKIE_OAUTH2_REQ) {
let mut authreq_cookie = authreq_cookie.clone();
authreq_cookie.make_removal();
authreq_cookie.set_path(Urls::Ui.as_ref());
jar.add(authreq_cookie)
} else {
jar
};
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
// If the auth_req was cross-signed, old, or just bad, error. But we have *cleared* it
// from the cookie which means we won't see it again.
@ -112,8 +102,9 @@ async fn oauth2_auth_req(
return (
jar,
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
err_code: OperationError::UI0003InvalidOauth2Resume,
operation_id: kopid.eventid,
domain_info,
},
)
.into_response();
@ -125,16 +116,8 @@ async fn oauth2_auth_req(
.await;
match res {
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
})) => {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
Ok(AuthoriseResponse::Permitted(success)) => {
let redirect_uri = success.build_redirect_uri();
(
jar,
@ -156,14 +139,17 @@ async fn oauth2_auth_req(
consent_token,
}) => {
// We can just render the form now, the consent token has everything we need.
ConsentRequestView {
client_name,
// scopes,
pii_scopes,
consent_token,
redirect: None,
}
.into_response()
(
jar,
ConsentRequestView {
client_name,
// scopes,
pii_scopes,
consent_token,
redirect: None,
},
)
.into_response()
}
Ok(AuthoriseResponse::AuthenticationRequired {
@ -172,16 +158,19 @@ async fn oauth2_auth_req(
}) => {
// Sign the auth req and hide it in our cookie - we'll come back for
// you later.
let maybe_jar =
cookies::make_signed(&state, COOKIE_OAUTH2_REQ, &auth_req, Urls::Ui.as_ref())
.map(|mut cookie| {
cookie.set_same_site(SameSite::Strict);
jar.add(cookie)
})
.ok_or(OperationError::InvalidSessionState);
let maybe_jar = cookies::make_signed(&state, COOKIE_OAUTH2_REQ, &auth_req)
.map(|mut cookie| {
cookie.set_same_site(SameSite::Strict);
// Expire at the end of the session.
cookie.set_expires(None);
// Could experiment with this to a shorter value, but session should be enough.
cookie.set_max_age(time::Duration::minutes(15));
jar.clone().add(cookie)
})
.ok_or(OperationError::InvalidSessionState);
match maybe_jar {
Ok(jar) => {
Ok(new_jar) => {
let display_ctx = LoginDisplayCtx {
domain_info,
oauth2: Some(Oauth2Ctx { client_name }),
@ -189,21 +178,28 @@ async fn oauth2_auth_req(
error: None,
};
super::login::view_oauth2_get(jar, display_ctx, login_hint)
super::login::view_oauth2_get(new_jar, display_ctx, login_hint)
}
Err(err_code) => UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
}
.into_response(),
Err(err_code) => (
jar,
UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
domain_info,
},
)
.into_response(),
}
}
Err(Oauth2Error::AccessDenied) => {
// If scopes are not available for this account.
AccessDeniedView {
operation_id: kopid.eventid,
}
.into_response()
(
jar,
AccessDeniedView {
operation_id: kopid.eventid,
},
)
.into_response()
}
/*
RFC - If the request fails due to a missing, invalid, or mismatching
@ -222,11 +218,15 @@ async fn oauth2_auth_req(
&err_code.to_string()
);
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
}
.into_response()
(
jar,
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
domain_info,
},
)
.into_response()
}
}
}
@ -240,44 +240,37 @@ pub struct ConsentForm {
}
pub async fn view_consent_post(
State(state): State<ServerState>,
State(server_state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar,
Form(consent_form): Form<ConsentForm>,
) -> Result<Response, UnrecoverableErrorView> {
let res = state
let res = server_state
.qe_w_ref
.handle_oauth2_authorise_permit(client_auth_info, consent_form.consent_token, kopid.eventid)
.await;
match res {
Ok(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
}) => {
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ);
Ok(success) => {
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &server_state);
if let Some(redirect) = consent_form.redirect {
Ok((
jar,
[
(HX_REDIRECT, redirect_uri.as_str().to_string()),
(HX_REDIRECT, success.redirect_uri.as_str().to_string()),
(
ACCESS_CONTROL_ALLOW_ORIGIN.as_str(),
redirect_uri.origin().ascii_serialization(),
success.redirect_uri.origin().ascii_serialization(),
),
],
Redirect::to(&redirect),
)
.into_response())
} else {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
let redirect_uri = success.build_redirect_uri();
Ok((
jar,
[
@ -302,6 +295,7 @@ pub async fn view_consent_post(
Err(UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
domain_info,
})
}
}

View file

@ -69,7 +69,7 @@ pub(crate) async fn view_profile_unlock_get(
.qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.await
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
let display_ctx = LoginDisplayCtx {
domain_info,

Some files were not shown because too many files have changed in this diff Show more