Python module and rewritten RADIUS integration (#826)

* added python kanidm module
* rewrote RADIUS integration
* updated the documentation
* updating github actions to run more often
* BLEEP BLOOP ASYNCIO IS GR8
* adding config to makefile to run pykanidm tests

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Firstyear <william@blackhats.net.au>
This commit is contained in:
James Hodgkinson 2022-06-20 20:16:55 +10:00 committed by GitHub
parent 1072edbb5e
commit 805ac2dd16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 4823 additions and 709 deletions

View file

@ -8,6 +8,13 @@ updates:
time: "06:00" time: "06:00"
timezone: Australia/Brisbane timezone: Australia/Brisbane
open-pull-requests-limit: 99 open-pull-requests-limit: 99
- package-ecosystem: pip
directory: "/pykanidm"
schedule:
interval: weekly
time: "06:00"
timezone: Australia/Brisbane
open-pull-requests-limit: 99
- package-ecosystem: cargo - package-ecosystem: cargo
directory: "/" directory: "/"
schedule: schedule:

View file

@ -1,105 +0,0 @@
---
name: Container for Kanidm
# this will build regardless,
# but only push to the container registry
# when you're committing on the master branch.
"on":
push:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux/arm64
- linux/amd64
steps:
- uses: actions/checkout@v3
- name: Update package manager
run: sudo apt-get update
- name: Install dependencies
run: |
sudo apt-get install -y \
libpam0g-dev \
libudev-dev \
libssl-dev \
libsqlite3-dev
- name: Install latest stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
components: cargo
- name: Run cargo test
run: cargo test -j1
kanidm_build:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux/arm64
- linux/amd64
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- # https://github.com/docker/login-action/#github-container-registry
name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push kanidmd
id: docker_build_kanidmd
uses: docker/build-push-action@v2
with:
push: ${{ github.ref == 'refs/heads/master' }}
platforms: ${{matrix.target}}
# https://github.com/docker/build-push-action/issues/254
tags: ghcr.io/kanidm/kanidmd:devel
build-args: |
"KANIDM_BUILD_PROFILE=container_generic"
"KANIDM_FEATURES="
"KANIDM_BUILD_OPTIONS=-j1"
file: kanidmd/Dockerfile
radius_build:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux/arm64
- linux/amd64
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- # https://github.com/docker/login-action/#github-container-registry
name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push radius
id: docker_build_radius
uses: docker/build-push-action@v2
with:
push: ${{ github.ref == 'refs/heads/master' }}
platforms: ${{matrix.target}}
# https://github.com/docker/build-push-action/issues/254
tags: ghcr.io/kanidm/radius:devel
context: ./kanidm_rlm_python/

View file

@ -0,0 +1,45 @@
---
name: Container - Kanidmd
# this will build regardless,
# but only push to the container registry
# when you're committing on the master branch.
"on":
push:
jobs:
kanidm_build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux/arm64
- linux/amd64
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- # https://github.com/docker/login-action/#github-container-registry
name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push kanidmd
id: docker_build_kanidmd
uses: docker/build-push-action@v2
with:
push: ${{ github.ref == 'refs/heads/master' }}
platforms: ${{matrix.target}}
# https://github.com/docker/build-push-action/issues/254
tags: ghcr.io/kanidm/kanidmd:devel
build-args: |
"KANIDM_BUILD_PROFILE=developer"
"KANIDM_FEATURES="
"KANIDM_BUILD_OPTIONS=-j1"
file: kanidmd/Dockerfile

View file

@ -0,0 +1,42 @@
---
name: Container - Radiusd
# this will build regardless,
# but only push to the container registry
# when you're committing on the master branch.
"on":
push:
jobs:
radius_build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux/arm64
- linux/amd64
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- # https://github.com/docker/login-action/#github-container-registry
name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push radius
id: docker_build_radius
uses: docker/build-push-action@v2
with:
push: ${{ github.ref == 'refs/heads/master' }}
platforms: ${{matrix.target}}
# https://github.com/docker/build-push-action/issues/254
tags: ghcr.io/kanidm/radius:devel
context: .
file: kanidm_rlm_python/Dockerfile

23
.github/workflows/pykanidm_mypy.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: pykanidm - mypy
"on":
push:
pull_request:
jobs:
pykanidm_mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Running mypy
run: |
cd pykanidm
python --version
python -m pip install --quiet --no-cache-dir --upgrade poetry
poetry install
poetry run mypy --strict kanidm tests

22
.github/workflows/pykanidm_pylint.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: pykanidm - pylint
"on":
push:
pull_request:
jobs:
pykanidm_pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Running tests
run: |
cd pykanidm
python -m pip install --quiet --no-cache-dir --upgrade poetry
poetry install
poetry run pylint tests kanidm

28
.github/workflows/pykanidm_pytest.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: pykanidm - pytest
"on":
push:
pull_request:
jobs:
pykanidm_pytest:
strategy:
matrix:
python_version:
- "3.8"
- "3.9"
- "3.10"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Set up Python ${{matrix.python_version}}
uses: actions/setup-python@v2
with:
python-version: ${{matrix.python_version}}
- name: Running pytest
run: |
cd pykanidm
python -m pip install --quiet --no-cache-dir --upgrade poetry
poetry install
poetry run pytest -v

12
.gitignore vendored
View file

@ -12,10 +12,16 @@ kanidm_rlm_python/test_data/certs/
vendor.tar.gz vendor.tar.gz
kanidm_rlm_python/test_data/ca.pem kanidm_rlm_python/test_data/ca.pem
loc.sh loc.sh
todo.sh
vendor.tar.* vendor.tar.*
*.patch *.patch
orca/example_profiles/small/orca-edited.toml orca/example_profiles/small/orca-edited.toml
docs/ /docs/
kanidm_unix_int/pam_tester/Cargo.lock kanidm_unix_int/pam_tester/Cargo.lock
.vscode/ .vscode/
# python things
**/__pycache__/**
**/.venv/**
.coverage
pykanidm/dist/
pykanidm/site/

2
CODEOWNERS Normal file
View file

@ -0,0 +1,2 @@
# yale-mistakes were made
/pykanidm/* @yaleman

View file

@ -1,4 +1,4 @@
.PHONY: help build/kanidmd build/radiusd test/kanidmd push/kanidmd push/radiusd vendor-prep doc install-tools prep vendor book clean_book .PHONY: help build/kanidmd build/radiusd test/kanidmd push/kanidmd push/radiusd vendor-prep doc install-tools prep vendor book clean_book test/pykanidm/pytest test/pykanidm/mypy test/pykanidm/pylint docs/pykanidm/build docs/pykanidm/serve
IMAGE_BASE ?= kanidm IMAGE_BASE ?= kanidm
IMAGE_VERSION ?= devel IMAGE_VERSION ?= devel
@ -24,37 +24,43 @@ buildx/kanidmd/x86_64_v3:
$(CONTAINER_BUILD_ARGS) . $(CONTAINER_BUILD_ARGS) .
@$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION) @$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION)
buildx/kanidmd: ## build multiarch server images buildx/kanidmd: ## Build multiarch kanidm server images and push to docker hub
buildx/kanidmd: buildx/kanidmd:
@$(CONTAINER_TOOL) buildx build $(CONTAINER_TOOL_ARGS) --pull --push --platform $(IMAGE_ARCH) \ @$(CONTAINER_TOOL) buildx build $(CONTAINER_TOOL_ARGS) \
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \ --pull --push --platform $(IMAGE_ARCH) \
-f kanidmd/Dockerfile \
-t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \ --build-arg "KANIDM_BUILD_PROFILE=container_generic" \
--build-arg "KANIDM_FEATURES=" \ --build-arg "KANIDM_FEATURES=" \
$(CONTAINER_BUILD_ARGS) . $(CONTAINER_BUILD_ARGS) .
@$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION) @$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION)
buildx/radiusd: ## build multiarch radius images buildx/radiusd: ## Build multi-arch radius docker images and push to docker hub
buildx/radiusd: buildx/radiusd:
@$(CONTAINER_TOOL) buildx build $(CONTAINER_TOOL_ARGS) --pull --push --platform $(IMAGE_ARCH) \ @$(CONTAINER_TOOL) buildx build $(CONTAINER_TOOL_ARGS) \
-f kanidm_rlm_python/Dockerfile -t $(IMAGE_BASE)/radius:$(IMAGE_VERSION) kanidm_rlm_python --pull --push --platform $(IMAGE_ARCH) \
-f kanidm_rlm_python/Dockerfile \
-t $(IMAGE_BASE)/radius:$(IMAGE_VERSION) .
@$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/radius:$(IMAGE_VERSION) @$(CONTAINER_TOOL) buildx imagetools $(CONTAINER_TOOL_ARGS) inspect $(IMAGE_BASE)/radius:$(IMAGE_VERSION)
buildx: buildx/kanidmd buildx/radiusd buildx: buildx/kanidmd buildx/radiusd
build/kanidmd: ## build kanidmd images build/kanidmd: ## Build the kanidmd docker image locally
build/kanidmd: build/kanidmd:
@$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) -f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \ @$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) -f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \ --build-arg "KANIDM_BUILD_PROFILE=container_generic" \
--build-arg "KANIDM_FEATURES=" \ --build-arg "KANIDM_FEATURES=" \
$(CONTAINER_BUILD_ARGS) . $(CONTAINER_BUILD_ARGS) .
build/radiusd: ## build radiusd image build/radiusd: ## Build the radiusd docker image locally
build/radiusd: build/radiusd:
@$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) -f kanidm_rlm_python/Dockerfile -t $(IMAGE_BASE)/radius:$(IMAGE_VERSION) kanidm_rlm_python @$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) \
-f kanidm_rlm_python/Dockerfile \
-t $(IMAGE_BASE)/radius:$(IMAGE_VERSION) .
build: build/kanidmd build/radiusd build: build/kanidmd build/radiusd
test/kanidmd: ## test kanidmd test/kanidmd: ## Run cargo test in docker
test/kanidmd: test/kanidmd:
@$(CONTAINER_TOOL) build \ @$(CONTAINER_TOOL) build \
$(CONTAINER_TOOL_ARGS) -f kanidmd/Dockerfile \ $(CONTAINER_TOOL_ARGS) -f kanidmd/Dockerfile \
@ -63,7 +69,12 @@ test/kanidmd:
$(CONTAINER_BUILD_ARGS) . $(CONTAINER_BUILD_ARGS) .
@$(CONTAINER_TOOL) run --rm $(IMAGE_BASE)/server:$(IMAGE_VERSION)-builder cargo test @$(CONTAINER_TOOL) run --rm $(IMAGE_BASE)/server:$(IMAGE_VERSION)-builder cargo test
# test/radiusd: build/radiusd ## test radiusd test/radiusd: ## Run a test radius server
cd kanidm_rlm_python && \
./run_radius_container.sh
test/radiusd: build/radiusd test/radiusd
vendor: vendor:
cargo vendor cargo vendor
@ -71,7 +82,7 @@ vendor:
vendor-prep: vendor vendor-prep: vendor
tar -cJf vendor.tar.xz vendor tar -cJf vendor.tar.xz vendor
doc: ## build doc local doc: ## Build the rust documentation locally
doc: doc:
cargo doc --document-private-items cargo doc --document-private-items
@ -106,3 +117,34 @@ prep:
cargo outdated -R cargo outdated -R
cargo audit cargo audit
test/pykanidm/pytest:
cd pykanidm && \
poetry install && \
poetry run pytest -vv
test/pykanidm/pylint:
cd pykanidm && \
poetry install && \
poetry run pylint tests kanidm
test/pykanidm/mypy:
cd pykanidm && \
poetry install && \
echo "Running mypy" && \
poetry run mypy --strict tests kanidm
test/pykanidm: ## run the test suite (mypy/pylint/pytest) for the kanidm python module
test/pykanidm: test/pykanidm/pytest test/pykanidm/mypy test/pykanidm/pylint
docs/pykanidm/build: ## Build the mkdocs
docs/pykanidm/build:
cd pykanidm && \
poetry install && \
poetry run mkdocs build
docs/pykanidm/serve: ## Run the local mkdocs server
docs/pykanidm/serve:
cd pykanidm && \
poetry install && \
poetry run mkdocs serve

46
examples/kanidm Normal file
View file

@ -0,0 +1,46 @@
# This should be at /etc/kanidm/config or ~/.config/kanidm,
# and configures kanidm clients including the FreeRADIUS server
uri = "https://idm.example.com"
# TODO: document this
# verify_ca = true
# enable (default) or disable TLS certificate verification
# verify_certificate = true
# enable (default) or disable TLS certificate hostname verification
# verify_hostnames = true
# an optional path to the CA certificate for the server URI above
# ca_path = "/etc/kanidm/cacert.pem"
# when configuring the FreeRADIUS server, set the service account details here
username = "radius_service_account"
password = "cr4bzr0ol"
# radius_cert_path = "/etc/raddb/certs/cert.pem" #
# radius_key_path = "/etc/raddb/certs/key.pem" # the signing key for radius TLS
# radius_dh_path = "/etc/raddb/certs/dh.pem" # the diffie-hellman output
# radius_ca_path = "/etc/raddb/certs/ca.pem" # the CA certificate?
# A list of groups, if a user is in them, they're approved for RADIUS authentication
radius_required_groups = [
"radius_access_allowed",
]
# A mapping between Kanidm groups and VLANS
radius_groups = [
{ name = "radius_access_allowed", vlan = 10 },
]
# The default VLAN if the user does not fit into another group
radius_default_vlan = 1
# A list of radius clients and their passwords, which are allowed to connect,
# typically network devices like switches and access points.
radius_clients = [
{ name = "test", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
# The client connection timeout, in seconds.
connect_timeout = 30

View file

@ -11,6 +11,7 @@ KEYFILE="${KANI_TMP}key.pem"
CERTFILE="${KANI_TMP}cert.pem" CERTFILE="${KANI_TMP}cert.pem"
CSRFILE="${KANI_TMP}cert.csr" CSRFILE="${KANI_TMP}cert.csr"
CHAINFILE="${KANI_TMP}chain.pem" CHAINFILE="${KANI_TMP}chain.pem"
DHFILE="${KANI_TMP}dh.pem"
if [ ! -d "${KANI_TMP}" ]; then if [ ! -d "${KANI_TMP}" ]; then
echo "Creating temp kanidm dir: ${KANI_TMP}" echo "Creating temp kanidm dir: ${KANI_TMP}"
@ -40,7 +41,7 @@ localityName_default = Brisbane
0.organizationName_default = INSECURE EXAMPLE 0.organizationName_default = INSECURE EXAMPLE
organizationalUnitName = Organizational Unit Name (eg, section) organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = KaniDM organizationalUnitName_default = kanidm
commonName = Common Name (eg, your name or your server\'s hostname) commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64 commonName_max = 64
@ -89,6 +90,9 @@ openssl x509 -req -days 31 \
# Create the chain # Create the chain
cat "${CERTFILE}" "${CACERT}" > "${CHAINFILE}" cat "${CERTFILE}" "${CACERT}" > "${CHAINFILE}"
# create the dh file for RADIUS
openssl dhparam -in "${CAFILE}" -out "${DHFILE}" 2048
echo "Certificate chain is at: ${CHAINFILE}" echo "Certificate chain is at: ${CHAINFILE}"
echo "Private key is at: ${KEYFILE}" echo "Private key is at: ${KEYFILE}"

View file

@ -18,6 +18,8 @@
# For Developers # For Developers
- [Developer Guide](DEVELOPER_README.md) - [Developer Guide](DEVELOPER_README.md)
- [Python Module](developers/python.md)
- [RADIUS Integration](developers/radius.md)
# Integrations # Integrations

View file

@ -0,0 +1,38 @@
# Kanidm Python Module
So far it includes:
- asyncio methods for all calls, leveraging [aiohttp](https://pypi.org/project/aiohttp/)
- every class and function is fully python typed (test by running `make test/pykanidm/mypy`)
- test coverage for 95% of code, and most of the missing bit is just when you break things
- loading configuration files into nice models using [pydantic](https://pypi.org/project/pydantic/)
- basic password authentication
- pulling RADIUS tokens
TODO: a lot of things.
## Setting up your dev environment.
Setting up a dev environment can be a little complex because of the mono-repo.
1. Install poetry: `python -m pip install poetry`. This is what we use to manage the packages, and allows you to set up virtual python environments easier.
2. Build the base environment. From within the `pykanidm` directory, run: `poetry install` This'll set up a virtual environment and install all the required packages (and development-related ones)
3. Start editing!
Most IDEs will be happier if you open the kanidm_rlm_python or pykanidm directories as the base you are working from, rather than the kanidm repository root, so they can auto-load integrations etc.
## Building the documentation
To build a static copy of the docs, run:
```shell
make docs/pykanidm/build
```
You can also run a local live server by running:
```shell
make docs/pykanidm/serve
```
This'll expose a web server at [http://localhost:8000](http://localhost:8000).

View file

@ -0,0 +1,67 @@
# RADIUS Module Development
Setting up a dev environment has some extra complexity due to the mono-repo design.
1. Install poetry: `python -m pip install poetry`. This is what we use to manage the packages, and allows you to set up virtual python environments easier.
2. Build the base environment. From within the kanidm_rlm_python directory, run: `poetry install`
3. Install the `kanidm` python library: `poetry run python -m pip install ../pykanidm`
4. Start editing!
Most IDEs will be happier if you open the `kanidm_rlm_python` or `pykanidm` directories as the base you are working from, rather than the `kanidm` repository root, so they can auto-load integrations etc.
## Running a test RADIUS container
From the root directory of the Kanidm repository:
1. Build the container - this'll give you a container image called `kanidm/radius` with the tag `devel`:
```shell
make build/radiusd
```
2. Once the process has completed, check the container exists in your docker environment:
```shell
➜ docker image ls kanidm/radius
REPOSITORY TAG IMAGE ID CREATED SIZE
kanidm/radius devel 5dabe894134c About a minute ago 622MB
```
*Note:* If you're just looking to play with a pre-built container, images are also automatically built based on the development branch and available at `ghcr.io/kanidm/radius:devel`
3. Generate some self-signed certificates by running the script - just hit enter on all the prompts if you don't want to customise them. This'll put the files in `/tmp/kanidm`:
```shell
./insecure_generate_tls.sh
```
4. Run the container:
```shell
cd kanidm_rlm_python && ./run_radius_container.sh
```
You can pass the following environment variables to `run_radius_container.sh` to set other options:
- IMAGE: an alternative image such as `ghcr.io/kanidm/radius:devel`
- CONFIG_FILE: mount your own config file
For example:
```shell
IMAGE=ghcr.io/kanidm/radius:devel \
CONFIG_FILE=~/.config/kanidm \
./run_radius_container.sh
```
## Testing authentication
Authentication can be tested through the client.localhost Network Access Server (NAS) configuration with:
```shell
docker exec -i -t radiusd radtest \
<username> badpassword \
127.0.0.1 10 testing123
docker exec -i -t radiusd radtest \
<username> <radius show_secret value here> \
127.0.0.1 10 testing123
```

View file

@ -82,112 +82,131 @@ To read these secrets, the RADIUS server requires an account with the
correct privileges. This can be created and assigned through the group correct privileges. This can be created and assigned through the group
"idm_radius_servers", which is provided by default. "idm_radius_servers", which is provided by default.
kanidm account create --name admin radius_service_account "Radius Service Account" First, create the account and add it to the group:
kanidm group add_members --name admin idm_radius_servers radius_service_account
kanidm account credential reset_credential --name admin radius_service_account ```shell
kanidm account create --name admin radius_service_account "Radius Service Account"
kanidm group add_members --name admin idm_radius_servers radius_service_account
```
Now reset the account password, using the `admin` account:
```shell
kanidm account credential update --name admin radius_service_account
```
## Deploying a RADIUS Container ## Deploying a RADIUS Container
We provide a RADIUS container that has all the needed integrations. We provide a RADIUS container that has all the needed integrations.
This container requires some cryptographic material, laid out in a volume like so: This container requires some cryptographic material, with the following files being in `/etc/raddb/certs`. (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 |
data The configuration file (`/data/kanidm`) has the following template:
data/ca.pem # This is the kanidm ca.pem
data/config.ini # This is the kanidm-radius configuration.
data/certs
data/certs/dh # openssl dhparam -out ./dh 2048
data/certs/key.pem # These are the radius ca/cert/key
data/certs/cert.pem
data/certs/ca.pem
The config.ini has the following template: ```toml
uri = "https://example.com" # URL to the Kanidm server
verify_hostnames = true # verify the hostname of the Kanidm server
[kanidm_client] verify_ca = false # Strict CA verification
url = # URL to the kanidm server ca = /data/ca.pem # Path to the kanidm ca
strict = false # Strict CA verification username = # Username of the RADIUS service account
ca = /data/ca.pem # Path to the kanidm ca password = # Generated secret for the service account
user = # Username of the RADIUS service account
secret = # Generated secret for the service account
; default VLANs for groups that don't specify one. # Default vlans for groups that don't specify one.
[DEFAULT] radius_default_vlan = 1
vlan = 1
; [group.test] # group.<name> will have these options applied # A list of Kanidm groups which must be a member
; vlan = # before they can authenticate via RADIUS.
radius_required_groups = [
"radius_access_allowed",
]
[radiusd] # A mapping between Kanidm groups and VLANS
ca = # Path to the radius server's CA radius_groups = [
key = # Path to the radius servers key { name = "radius_access_allowed", vlan = 10 },
cert = # Path to the radius servers cert ]
dh = # Path to the radius servers dh params
required_group = # Name of a kanidm group which you must be
# A member of to use radius.
cache_path = # A path to an area where cached user records can be stored.
# If in doubt, use /dev/shm/kanidmradiusd
; [client.localhost] # client.<nas name> configures wifi/vpn consumers # A mapping of clients and their authentication tokens
; ipaddr = # ipv4 or ipv6 address of the NAS radius_clients = [
; secret = # Shared secret { name = "test", ipaddr = "127.0.0.1", secret = "testing123" },
# TODO: see if this works - it gets written out to the file
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
A fully configured example: # 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"
[kanidm_client] ```
; be sure to check the listening port is correct, it's the docker internal port
; not the external one if these containers are on the same host.
url = https://<kanidmd container name or ip>:8443
strict = true # Adjust this if you have CA validation issues
ca = /data/ca.crt
user = radius_service_account
secret = # The generated password from above
; default vlans for groups that don't specify one. ## A fully configured example
[DEFAULT]
vlan = 1
[group.network_admins]
vlan = 10
[radiusd] ```toml
ca = /data/certs/ca.pem url = "https://example.com"
key = /data/certs/key.pem
cert = /data/certs/cert.pem
dh = /data/certs/dh
required_group = radius_access_allowed
cache_path = /dev/shm/kanidmradiusd
[client.localhost] username = "radius_service_account"
ipaddr = 127.0.0.1 # The generated password from above
secret = testing123 password = "cr4bzr0ol"
[client.docker] # default vlan for groups that don't specify one.
ipaddr = 172.17.0.0/16 radius_default_vlan = 99
secret = testing123
You can then run the container with: # if the user is in one of these Kanidm groups,
# then they're allowed to authenticate
radius_required_groups = [
"radius_access_allowed",
]
docker run --name radiusd -v ...:/data kanidm/radius:latest radius_groups = [
{ name = "radius_access_allowed", vlan = 10 }
]
Authentication can be tested through the client.localhost Network Access Server (NAS) configuration with: radius_clients = [
{ name = "localhost", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
```
## Moving to Production
docker exec -i -t radiusd radtest <username> badpassword 127.0.0.1 10 testing123 To expose this to a Wi-Fi infrastructure, add your NAS in the configuration:
docker exec -i -t radiusd radtest <username> <radius show_secret value here> 127.0.0.1 10 testing123
Finally, to expose this to a Wi-Fi infrastructure, add your NAS in `config.ini`: ```toml
radius_clients = [
{ name = "access_point", ipaddr = "10.2.3.4", secret = "<a_random_value>" }
]
```
[client.access_point] Then re-create/run your docker instance and expose the ports by adding
ipaddr = <some ipadd> `-p 1812:1812 -p 1812:1812/udp` to the command.
secret = <random value>
Then re-create/run your docker instance with `-p 1812:1812 -p 1812:1812/udp` ...
If you have any issues, check the logs from the RADIUS output, as they tend If you have any issues, check the logs from the RADIUS output, as they tend
to indicate the cause of the problem. To increase the logging level you can to indicate the cause of the problem. To increase the logging level you can
re-run your environment with debug enabled: re-run your environment with debug enabled:
docker rm radiusd ```shell
docker run --name radiusd -e DEBUG=True -i -t -v ...:/data kanidm/radius:latest docker rm radiusd
docker run --name radiusd \
-e DEBUG=True \
-p 1812:1812 \
-p 1812:1812/udp
--interactive --tty \
--volume /tmp/kanidm:/etc/raddb/certs \
kanidm/radius:latest
```
Note the RADIUS container *is* configured to provide Tunnel-Private-Group-ID, 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 so if you wish to use Wi-Fi-assigned VLANs on your infrastructure, you can
assign these by groups in the config.ini as shown in the above examples. assign these by groups in the configuration file as shown in the above examples.

View file

@ -1,42 +1,57 @@
FROM opensuse/leap:latest FROM opensuse/tumbleweed:latest
LABEL org.opencontainers.image.authors="william@blackhats.net.au"
EXPOSE 1812 1813 EXPOSE 1812 1813
RUN zypper --gpg-auto-import-keys ref --force RUN zypper --gpg-auto-import-keys refresh --force
RUN zypper refresh RUN zypper install -y \
RUN zypper install -y timezone freeradius-client freeradius-server freeradius-server-ldap \ freeradius-client \
freeradius-server-python3 openldap2-client freeradius-server-utils hostname \ freeradius-server \
python3 python3-requests python3-devel \ freeradius-server-python3 \
iproute2 iputils curl && \ freeradius-server-utils \
zypper clean hostname \
python3 \
# Copy the python module to /etc/raddb python3-devel \
COPY kanidmradius.py /etc/raddb/ python3-pip \
COPY entrypoint.py /entrypoint.py timezone \
iproute2 \
# Copy in the python changes, as well as the default/inner-tunnel changes iputils \
COPY mod-python3 /etc/raddb/mods-available/python3 curl
COPY eap /etc/raddb/mods-available/eap RUN zypper clean
COPY cache /etc/raddb/mods-available/cache
COPY default /etc/raddb/sites-available/default
COPY inner-tunnel /etc/raddb/sites-available/inner-tunnel
# Enable the python and cache module.
RUN ln -s ../mods-available/python3 /etc/raddb/mods-enabled/python3
# RUN ln -s ../mods-available/cache /etc/raddb/mods-enabled/cache
# Allows radiusd (?) to write to the directory
RUN chown -R radiusd: /etc/raddb && \
chmod 775 /etc/raddb/certs && \
chmod 640 /etc/raddb/clients.conf
ADD kanidm_rlm_python/mods-available/ /etc/raddb/mods-available/
COPY kanidm_rlm_python/sites-available/ /etc/raddb/sites-available/
# Set a working directory of /etc/raddb # Set a working directory of /etc/raddb
WORKDIR /etc/raddb WORKDIR /etc/raddb
# /data volume # Enable the python and cache module.
VOLUME /data RUN ln -s ../mods-available/python3 /etc/raddb/mods-enabled/python3
# disable auth via methods we don't support!
RUN rm /etc/raddb/mods-available/sql
RUN rm /etc/raddb/mods-enabled/{passwd,totp}
# Allows the radiusd user to write to the directory
RUN chown -R radiusd: /etc/raddb
RUN chmod 775 /etc/raddb/certs
RUN chmod 640 /etc/raddb/clients.conf
# install the packages
RUN mkdir -p /pkg/kanidmradius/kanidmradius/
COPY kanidm_rlm_python//kanidmradius/ /pkg/kanidmradius/kanidmradius/
COPY kanidm_rlm_python/pyproject.toml /pkg/kanidmradius/
RUN mkdir -p /pkg/pykanidm/
COPY pykanidm/ /pkg/pykanidm/
# install the package and its dependencies
RUN ln -s /etc/raddb/mods-config/python3/radiusd.py /usr/lib/python3.8/site-packages/
RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/pykanidm
RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/kanidmradius
# clean up after install
RUN rm -rf /pkg/*
USER radiusd USER radiusd
COPY kanidm_rlm_python/entrypoint.py /entrypoint.py
CMD [ "/usr/bin/python3", "/entrypoint.py" ] CMD [ "/usr/bin/python3", "/entrypoint.py" ]

View file

@ -1,27 +0,0 @@
Testing Process
===============
cd kanidmd
cargo run -- recover_account -c ./server.toml -n admin
cargo run -- server -c ./server.toml
cd kanidm_tools
cargo run -- login -D admin
cargo run -- account list -D admin
cargo run -- account create -D admin radius_service_account radius_service_account
cargo run -- group add_members -D admin idm_radius_servers radius_service_account
cargo run -- account credential set_password radius_service_account -D admin
cargo run -- account radius generate_secret admin -D admin
cd kanidm_rlm_python/
KANIDM_RLM_CONFIG=./test_data/config.ini python3 kanidmradius.py test
KANIDM_RLM_CONFIG=./test_data/config.ini python3 kanidmradius.py admin

View file

@ -1,96 +1,135 @@
import sys """ entrypoint for kanidm's RADIUS module """
import os
import subprocess
import atexit import atexit
import os
from pathlib import Path
import subprocess
import shutil import shutil
import signal import signal
import sys
from typing import Any
# import toml
from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config
MAJOR, MINOR, _, _, _ = sys.version_info DEBUG = True
if MAJOR >= 3:
import configparser
else:
import ConfigParser as configparser
DEBUG = False
if os.environ.get('DEBUG', False): if os.environ.get('DEBUG', False):
DEBUG = True DEBUG = True
CONFIG = configparser.ConfigParser() # pylint: disable=unused-argument
CONFIG.read('/data/config.ini') def _sigchild_handler(
*args: Any,
CLIENTS = [ **kwargs: Any,
{ ) -> None:
"name": x.split('.')[1], """ handler for SIGCHLD call"""
"secret": CONFIG.get(x, "secret"), print("Received SIGCHLD ...", file=sys.stderr)
"ipaddr": CONFIG.get(x, "ipaddr"),
}
for x in CONFIG.sections()
if x.startswith('client.')
]
print(CLIENTS)
def _sigchild_handler(*args, **kwargs):
# log.debug("Received SIGCHLD ...")
os.waitpid(-1, os.WNOHANG) os.waitpid(-1, os.WNOHANG)
def write_clients_conf(): def write_clients_conf(
with open('/etc/raddb/clients.conf', 'w') as f: kanidm_config_object: KanidmClientConfig,
for client in CLIENTS: ) -> None:
f.write('client %s {\n' % client['name']) """ writes out the config file """
f.write(' ipaddr = %s\n' % client['ipaddr']) raddb_config_file = Path("/etc/raddb/clients.conf")
f.write(' secret = %s\n' % client['secret'])
f.write(' proto = *\n')
f.write('}\n')
def setup_certs(): with raddb_config_file.open('w', encoding='utf-8') as file_handle:
for client in kanidm_config_object.radius_clients:
file_handle.write(f"client {client.name} {{\n" )
file_handle.write(f" ipaddr = {client.ipaddr}\n")
file_handle.write(f" secret = {client.secret}\n" )
file_handle.write(' proto = *\n')
file_handle.write('}\n')
def setup_certs(
kanidm_config_object: KanidmClientConfig,
) -> None:
""" sets up certificates """
# copy ca to /etc/raddb/certs/ca.pem # copy ca to /etc/raddb/certs/ca.pem
shutil.copyfile(CONFIG.get("radiusd", "ca"), '/etc/raddb/certs/ca.pem') if kanidm_config_object.ca_path:
shutil.copyfile(CONFIG.get("radiusd", "dh"), '/etc/raddb/certs/dh') cert_ca = Path(kanidm_config_object.ca_path).expanduser().resolve()
# concat key + cert into /etc/raddb/certs/server.pem if not cert_ca.exists():
with open('/etc/raddb/certs/server.pem', 'w') as f: print(f"Failed to find radiusd ca file ({cert_ca}), quitting!", file=sys.stderr)
with open(CONFIG.get("radiusd", "key"), 'r') as r: sys.exit(1)
f.write(r.read())
f.write('\n')
with open(CONFIG.get("radiusd", "cert"), 'r') as r:
f.write(r.read())
def run_radiusd():
global proc
if DEBUG:
proc = subprocess.Popen([
"/usr/sbin/radiusd", "-X"
], stderr=subprocess.STDOUT)
else:
proc = subprocess.Popen([
"/usr/sbin/radiusd", "-f",
"-l", "stdout"
], stderr=subprocess.STDOUT)
print(proc)
def kill_radius():
if proc is None:
pass
else: else:
try: print(f"Looking for cert_ca in {cert_ca}", file=sys.stderr )
os.kill(proc.pid, signal.SIGTERM) shutil.copyfile(cert_ca, '/etc/raddb/certs/ca.pem')
except: if kanidm_config_object.radius_dh_path is not None:
# It's already gone ... # if CONFIG.get("radiusd", "dh", fallback="") != "":
pass cert_dh = Path(kanidm_config_object.radius_dh_path).expanduser().resolve()
print("Stopping radiusd ...") if not cert_dh.exists():
# To make sure we really do shutdown, we actually re-block on the proc print(f"Failed to find radiusd dh file ({cert_dh}), quitting!", file=sys.stderr)
# again here to be sure it's done. sys.exit(1)
proc.wait() shutil.copyfile(cert_dh, '/etc/raddb/certs/dh')
atexit.register(kill_radius) server_key = Path(kanidm_config_object.radius_key_path).expanduser().resolve()
if not server_key.exists() or not server_key.is_file():
print(
f"Failed to find server keyfile ({server_key}), quitting!",
file=sys.stderr,
)
sys.exit(1)
server_cert = Path(kanidm_config_object.radius_cert_path).expanduser().resolve()
if not server_cert.exists() or not server_cert.is_file():
print(
f"Failed to find server cert file ({server_cert}), quitting!",
file=sys.stderr,
)
sys.exit(1)
# concat key + cert into /etc/raddb/certs/server.pem
with open('/etc/raddb/certs/server.pem', 'w', encoding='utf-8') as file_handle:
file_handle.write(server_cert.read_text(encoding="utf-8"))
file_handle.write('\n')
file_handle.write(server_key.read_text(encoding="utf-8"))
def kill_radius(
proc: subprocess.Popen,
) -> None:
""" handler to kill the radius server once the script exits """
if proc is None:
pass
else:
try:
os.kill(proc.pid, signal.SIGTERM)
except OSError:
print("sever is already gone...", file=sys.stderr)
print("Stopping radiusd ...", file=sys.stderr)
# To make sure we really do shutdown, we actually re-block on the proc
# again here to be sure it's done.
proc.wait() proc.wait()
def run_radiusd() -> None:
""" run the server """
if DEBUG:
cmd_args = [ "-X" ]
else:
cmd_args = [ "-f", "-l", "stdout" ]
with subprocess.Popen(
["/usr/sbin/radiusd"] + cmd_args,
stderr=subprocess.STDOUT,
) as proc:
# print(proc, file=sys.stderr)
atexit.register(kill_radius, proc)
proc.wait()
if __name__ == '__main__': if __name__ == '__main__':
signal.signal(signal.SIGCHLD, _sigchild_handler) signal.signal(signal.SIGCHLD, _sigchild_handler)
setup_certs()
write_clients_conf()
run_radiusd()
config_file = Path("/data/config.ini").expanduser().resolve()
if not config_file.exists:
print(
"Failed to find configuration file ({config_file}), quitting!",
file=sys.stderr,
)
sys.exit(1)
kanidm_config = KanidmClientConfig.parse_obj(load_config('/data/kanidm'))
setup_certs(kanidm_config)
write_clients_conf(kanidm_config)
print("Configuration set up, starting...")
try:
run_radiusd()
except KeyboardInterrupt as ki:
print(ki)

View file

@ -1,179 +0,0 @@
import sys
import requests
import logging
import os
import json
MAJOR, MINOR, _, _, _ = sys.version_info
if MAJOR >= 3:
import configparser
from functools import reduce
else:
import ConfigParser as configparser
# Setup the config too
print(os.getcwd())
CONFIG_PATH = os.environ.get('KANIDM_RLM_CONFIG', '/data/config.ini')
CONFIG = configparser.ConfigParser()
CONFIG.read(CONFIG_PATH)
GROUPS = [
{
"name": x.split('.')[1],
"vlan": CONFIG.get(x, "vlan")
}
for x in CONFIG.sections()
if x.startswith('group.')
]
REQ_GROUP = CONFIG.get("radiusd", "required_group")
if CONFIG.getboolean("kanidm_client", "strict"):
CA = CONFIG.get("kanidm_client", "ca", fallback=True)
else:
CA = False
USER = CONFIG.get("kanidm_client", "user")
SECRET = CONFIG.get("kanidm_client", "secret")
DEFAULT_VLAN = CONFIG.get("radiusd", "vlan")
TIMEOUT = 8
URL = CONFIG.get('kanidm_client', 'url')
AUTH_URL = "%s/v1/auth" % URL
def _authenticate(s, acct, pw):
init_auth = {"step": {"init": acct}}
r = s.post(AUTH_URL, json=init_auth, verify=CA, timeout=TIMEOUT)
if r.status_code != 200:
print(r.json())
raise Exception("AuthInitFailed")
session_id = r.headers["x-kanidm-auth-session-id"]
headers = {"X-KANIDM-AUTH-SESSION-ID": session_id}
# {'sessionid': '00000000-5fe5-46e1-06b6-b830dd035a10', 'state': {'choose': ['password']}}
if 'password' not in r.json().get('state', {'choose': None}).get('choose', None):
print("invalid auth mech presented %s" % r.json())
raise Exception("AuthMechUnknown")
begin_auth = {"step": {"begin": "password"}}
r = s.post(AUTH_URL, json=begin_auth, verify=CA, timeout=TIMEOUT, headers=headers)
if r.status_code != 200:
print(r.json())
raise Exception("AuthBeginFailed")
cred_auth = {"step": { "cred": {"password": pw}}}
r = s.post(AUTH_URL, json=cred_auth, verify=CA, timeout=TIMEOUT, headers=headers)
response = r.json()
if r.status_code != 200:
print(response)
raise Exception("AuthCredFailed")
response = r.json()
# Get the token
try:
token = response['state']['success']
return token
except KeyError:
print(response)
raise Exception("AuthCredFailed")
def _get_radius_token(username):
print("getting rtok for %s ..." % username)
s = requests.session()
# First authenticate a connection
bearer_token = _authenticate(s, USER, SECRET)
# Now get the radius token
rtok_url = "%s/v1/account/%s/_radius/_token" % (URL, username)
headers = {'Authorization': 'Bearer %s' % bearer_token}
r = s.get(rtok_url, verify=CA, timeout=TIMEOUT, headers=headers)
if r.status_code != 200:
print(r.status_code)
print(r.json())
raise Exception("Failed to get RadiusAuthToken")
else:
return r.json()
def check_vlan(acc, group):
if CONFIG.has_section("group.%s" % group['name']):
if CONFIG.has_option("group.%s" % group['name'], "vlan"):
v = CONFIG.get("group.%s" % group['name'], "vlan")
print("assigning vlan %s from %s" % (v,group))
return v
return acc
def instantiate(args):
print(args)
return radiusd.RLM_MODULE_OK
def authorize(args):
radiusd.radlog(radiusd.L_INFO, 'kanidm python module called')
dargs = dict(args)
# print(dargs)
username = dargs['User-Name']
tok = None
try:
tok = _get_radius_token(username)
except Exception as e:
radiusd.radlog(radiusd.L_INFO, 'kanidm exception %s' % e)
if tok == None:
radiusd.radlog(radiusd.L_INFO, 'kanidm RLM_MODULE_NOTFOUND due to no auth token')
return radiusd.RLM_MODULE_NOTFOUND
# print("got token %s" % tok)
# Are they in the required group?
req_sat = False
for group in tok["groups"]:
if group['name'] == REQ_GROUP:
req_sat = True
radiusd.radlog(radiusd.L_INFO, "required group satisfied -> %s:%s" % (username, req_sat))
if req_sat is not True:
return radiusd.RLM_MODULE_NOTFOUND
# look up them in config for group vlan if possible.
uservlan = reduce(check_vlan, tok["groups"], DEFAULT_VLAN)
if uservlan == 0:
radiusd.radlog(radiusd.L_INFO, "Invalid uservlan of 0")
radiusd.radlog(radiusd.L_INFO, "selected vlan %s:%s" % (username, uservlan))
# Convert the tok groups to groups.
name = tok["name"]
secret = tok["secret"]
reply = (
('User-Name', str(name)),
('Reply-Message', 'Welcome'),
('Tunnel-Type', '13'),
('Tunnel-Medium-Type', '6'),
('Tunnel-Private-Group-ID', str(uservlan)),
)
config = (
('Cleartext-Password', str(secret)),
)
radiusd.radlog(radiusd.L_INFO, "OK! Returning details to radius for %s ..." % username)
return (radiusd.RLM_MODULE_OK, reply, config)
if __name__ == '__main__':
# Test getting from the kanidm server instead.
if len(sys.argv) != 2:
print("usage: %s username" % sys.argv[0])
else:
tok = _get_radius_token(sys.argv[1])
print(tok)
print(tok["groups"])
else:
import radiusd

View file

@ -0,0 +1,199 @@
""" kanidm RADIUS module """
import asyncio
from functools import reduce
import json
import logging
import os
from pathlib import Path
import sys
from typing import Any, Dict, Optional, Union
import aiohttp
from kanidm import KanidmClient
from kanidm.types import AuthStepPasswordResponse
from kanidm.utils import load_config
from kanidm.exceptions import NoMatchingEntries
from . import radiusd
logging.basicConfig(
level=logging.DEBUG,
stream=sys.stderr,
)
# 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
]
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)
sys.exit(1)
config = load_config(str(CONFIG_PATH))
COOKIE_JAR = aiohttp.CookieJar()
KANIDM_CLIENT = KanidmClient(config_file=CONFIG_PATH)
def authenticate(
acct: str,
password: str,
kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Union[int, AuthStepPasswordResponse]:
""" authenticate the RADIUS service account to Kanidm """
logging.error("authenticate - %s:%s", acct, password)
try:
loop = asyncio.get_event_loop()
with aiohttp.client.ClientSession(cookie_jar=COOKIE_JAR) as session:
kanidm_client.session = session
return loop.run_until_complete(kanidm_client.authenticate_password(
username=acct,
password=password
))
except Exception as error_message: #pylint: disable=broad-except
logging.error("Failed to run kanidm.authenticate_password: %s", error_message)
return radiusd.RLM_MODULE_FAIL
async def _get_radius_token(
username: Optional[str]=None,
kanidm_client: KanidmClient=KANIDM_CLIENT,
) -> Optional[Dict[str, Any]]:
if username is None:
raise ValueError("Didn't get a username for _get_radius_token")
# authenticate as the radius service account
logging.debug("Authenticating kanidm radius service account")
radius_auth_response = await kanidm_client.authenticate_password()
logging.debug("Getting RADIUS token for %s", username)
response = await kanidm_client.get_radius_token(
username=username,
radius_session_id = radius_auth_response.sessionid,
)
logging.debug("Got radius token for %s", username)
if response.status_code != 200:
logging.error("got response status code: %s", response.status_code)
logging.error("Response content: %s", response.json())
raise Exception("Failed to get RadiusAuthToken")
logging.debug("Success getting RADIUS token: %s", response.json())
print(response.data)
return response.data
def check_vlan(
acc: int,
group: Dict[str, str],
kanidm_client: Optional[KanidmClient] = None,
) -> int:
""" checks if a vlan is in the config,
acc is the default vlan
"""
logging.debug("acc=%s", acc)
if kanidm_client is None:
kanidm_client = KANIDM_CLIENT
# raise ValueError("Need to pass this a kanidm_client")
for radius_group in kanidm_client.config.radius_groups:
logging.debug("Checking '%s' radius_group against group %s", radius_group, group['name'])
if radius_group.name == group['name']:
return radius_group.vlan
#if CONFIG.has_section(f"group.{group['name']}"):
# if CONFIG.has_option(f"group.{group['name']}", "vlan"):
# vlan = CONFIG.getint(f"group.{group['name']}", "vlan")
# logging.debug("assigning vlan %s from group %s", vlan, group)
# return vlan
logging.debug("returning default vlan: %s", acc)
return acc
def instantiate(_: Any) -> Any:
""" start up radiusd """
logging.info("Starting up!")
return radiusd.RLM_MODULE_OK
def authorize(
args: Any=Dict[Any,Any],
kanidm_client: KanidmClient=KANIDM_CLIENT,
) -> Any:
""" does the kanidm authorize step """
logging.info('kanidm python module called')
# args comes in like this
# (
# ('User-Name', '<username>'),
# ('User-Password', '<radius_password>'),
# ('NAS-IP-Address', '<client IP>'),
# ('NAS-Port', '<the'),
# ('Message-Authenticator', '0xaabbccddeeff00112233445566778899'),
# ('Event-Timestamp', 'Jun 9 2022 12:07:50 UTC')
# )
dargs = dict(args)
logging.error("Authorise: %s", json.dumps(dargs))
username = dargs['User-Name']
tok = None
try:
loop = asyncio.get_event_loop()
tok = loop.run_until_complete(_get_radius_token(username=username))
logging.debug("radius_token: %s", tok)
except NoMatchingEntries as error_message:
logging.info(
'kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user %s: %s',
username,
error_message,
)
return radiusd.RLM_MODULE_NOTFOUND
except Exception as error_message: # pylint: disable=broad-except
logging.error("kanidm exception: %s, %s", type(error_message), error_message)
if tok is None:
logging.info('kanidm RLM_MODULE_NOTFOUND due to no auth token')
return radiusd.RLM_MODULE_NOTFOUND
# Are they in the required group?
req_sat = False
for group in tok["groups"]:
if group['name'] in kanidm_client.config.radius_required_groups:
req_sat = True
logging.info("User %s has a required group (%s)", username, group['name'])
if req_sat is not True:
logging.info("User %s doesn't have a group from the required list.", username)
return radiusd.RLM_MODULE_NOTFOUND
# look up them in config for group vlan if possible.
#TODO: work out the typing on this, WTF.
uservlan: int = reduce(
check_vlan,
tok["groups"],
kanidm_client.config.radius_default_vlan,
)
if uservlan == int(0):
logging.info("Invalid uservlan of 0")
logging.info("selected vlan %s:%s", username, uservlan)
# Convert the tok groups to groups.
name = tok["name"]
secret = tok["secret"]
reply = (
('User-Name', str(name)),
('Reply-Message', 'Welcome'),
('Tunnel-Type', '13'),
('Tunnel-Medium-Type', '6'),
('Tunnel-Private-Group-ID', str(uservlan)),
)
config_object = (
('Cleartext-Password', str(secret)),
)
logging.info("OK! Returning details to radius for %s ...", username)
return (radiusd.RLM_MODULE_OK, reply, config_object)

View file

@ -0,0 +1,39 @@
""" this was pulled from freeradius """
#!/usr/bin/python3
#
# Definitions for RADIUS programs
#
# Copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com>
#
# This should only be used when testing modules.
# Inside freeradius, the 'radiusd' Python module is created by the C module
# and the definitions are automatically created.
#
# $Id: e9db28a4fa7dc8fe163a1d1a1dcf23771ef32990 $
# from modules.h
RLM_MODULE_REJECT = 0
RLM_MODULE_FAIL = 1
RLM_MODULE_OK = 2
RLM_MODULE_HANDLED = 3
RLM_MODULE_INVALID = 4
RLM_MODULE_USERLOCK = 5
RLM_MODULE_NOTFOUND = 6
RLM_MODULE_NOOP = 7
RLM_MODULE_UPDATED = 8
RLM_MODULE_NUMCODES = 9
# from log.h
L_AUTH = 2
L_INFO = 3
L_ERR = 4
L_WARN = 5
L_PROXY = 6
L_ACCT = 7
L_DBG = 16
L_DBG_WARN = 17
L_DBG_ERR = 18
L_DBG_WARN_REQ = 19
L_DBG_ERR_REQ = 20

View file

@ -0,0 +1,20 @@
""" utility functions """
import logging
import sys
from pathlib import Path
from typing import Dict, Any
import toml
def load_config(filename: str="/etc/kanidm/config") -> Dict[str, Any]:
""" loads the configuration file """
config_filepath = Path(filename).expanduser().resolve()
if not config_filepath.exists():
print(f"what {config_filepath}")
logging.error("Failed to find configuration file (%s), quitting!", config_filepath)
sys.exit(1)
config_data: Dict[str, Any] = toml.load(config_filepath.open(encoding="utf-8"))
return config_data

View file

@ -105,8 +105,8 @@ eap {
# User-Password, or the NT-Password attributes. # User-Password, or the NT-Password attributes.
# 'System' authentication is impossible with LEAP. # 'System' authentication is impossible with LEAP.
# #
leap { #leap {
} #}
# Generic Token Card. # Generic Token Card.
# #
@ -236,7 +236,7 @@ eap {
# #
# openssl dhparam -out certs/dh 2048 # openssl dhparam -out certs/dh 2048
# #
dh_file = ${certdir}/dh dh_file = ${certdir}/dh.pem
# #
# If your system doesn't have /dev/urandom, # If your system doesn't have /dev/urandom,

View file

@ -13,9 +13,10 @@ python3 {
# item is GLOBAL TO THE SERVER. That is, you cannot have two # item is GLOBAL TO THE SERVER. That is, you cannot have two
# instances of the python module, each with a different path. # instances of the python module, each with a different path.
# #
python_path="/usr/lib64/python3.6:/usr/lib/python3.6:/usr/lib/python3.6/site-packages:/usr/lib64/python3.6/site-packages:/usr/lib64/python3.6/lib-dynload:/etc/raddb" python_path="/usr/lib64/python3.8:/usr/lib/python3.8:/usr/lib/python3.8/site-packages:/usr/lib64/python3.8/site-packages:/usr/lib64/python3.8/lib-dynload:/usr/local/lib/python3.8/site-packages:/etc/raddb/mods-config/python3/"
module = kanidmradius module = kanidmradius
# python_path = ${modconfdir}/${.:name}
# Pass all VPS lists as a 6-tuple to the callbacks # Pass all VPS lists as a 6-tuple to the callbacks
# (request, reply, config, state, proxy_req, proxy_reply) # (request, reply, config, state, proxy_req, proxy_reply)
@ -30,36 +31,36 @@ python3 {
mod_instantiate = ${.module} mod_instantiate = ${.module}
func_instantiate = instantiate func_instantiate = instantiate
mod_detach = ${.module} #mod_detach = ${.module}
# func_detach = detach #func_detach = detach
mod_authorize = ${.module} mod_authorize = ${.module}
func_authorize = authorize func_authorize = authorize
mod_authenticate = ${.module} mod_authenticate = ${.module}
# func_authenticate = authenticate func_authenticate = authenticate
mod_preacct = ${.module} #mod_preacct = ${.module}
# func_preacct = preacct #func_preacct = preacct
mod_accounting = ${.module} #mod_accounting = ${.module}
# func_accounting = accounting #func_accounting = accounting
mod_checksimul = ${.module} #mod_checksimul = ${.module}
# func_checksimul = checksimul #func_checksimul = checksimul
mod_pre_proxy = ${.module} #mod_pre_proxy = ${.module}
# func_pre_proxy = pre_proxy #func_pre_proxy = pre_proxy
mod_post_proxy = ${.module} #mod_post_proxy = ${.module}
# func_post_proxy = post_proxy #func_post_proxy = post_proxy
mod_post_auth = ${.module} #mod_post_auth = ${.module}
# func_post_auth = post_auth #func_post_auth = post_auth
mod_recv_coa = ${.module} #mod_recv_coa = ${.module}
# func_recv_coa = recv_coa #func_recv_coa = recv_coa
mod_send_coa = ${.module} #mod_send_coa = ${.module}
# func_send_coa = send_coa #func_send_coa = send_coa
} }

View file

@ -1,3 +1,34 @@
[[package]]
name = "aiohttp"
version = "3.8.1"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = ">=4.0.0a3,<5.0"
attrs = ">=17.3.0"
charset-normalizer = ">=2.0,<3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotli", "cchardet"]
[[package]]
name = "aiosignal"
version = "1.2.0"
description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.11.5" version = "2.11.5"
@ -8,17 +39,46 @@ python-versions = ">=3.6.2"
[package.dependencies] [package.dependencies]
lazy-object-proxy = ">=1.4.0" lazy-object-proxy = ">=1.4.0"
typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
wrapt = ">=1.11,<2" wrapt = ">=1.11,<2"
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2021.10.8" version = "2022.5.18.1"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = ">=3.6"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
@ -41,15 +101,23 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "dill" name = "dill"
version = "0.3.4" version = "0.3.5.1"
description = "serialize all of python" description = "serialize all of python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
[package.extras] [package.extras]
graph = ["objgraph (>=1.7.2)"] graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "frozenlist"
version = "1.3.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.3" version = "3.3"
@ -58,6 +126,14 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.10.1" version = "5.10.1"
@ -88,17 +164,82 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "multidict"
version = "6.0.2"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mypy"
version = "0.960"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "2.4.0" version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.extras] [package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] dev = ["pre-commit", "tox"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pylint" name = "pylint"
@ -121,6 +262,52 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""
[package.extras] [package.extras]
testutil = ["gitpython (>3)"] testutil = ["gitpython (>3)"]
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "7.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.18.3"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
pytest = ">=6.1.0"
[package.extras]
testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.27.1" version = "2.27.1"
@ -139,29 +326,56 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "1.2.3" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
[[package]] [[package]]
name = "typed-ast" name = "types-requests"
version = "1.5.2" version = "2.27.29"
description = "a fork of Python 2 and 3 ast modules with type comment support" description = "Typing stubs for requests"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = "*"
[package.dependencies]
types-urllib3 = "<1.27"
[[package]]
name = "types-toml"
version = "0.10.7"
description = "Typing stubs for toml"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "types-urllib3"
version = "1.26.15"
description = "Typing stubs for urllib3"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.1.1" version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.6+" description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
[[package]] [[package]]
name = "urllib3" name = "urllib3"
@ -178,25 +392,127 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "wrapt" name = "wrapt"
version = "1.14.0" version = "1.14.1"
description = "Module for decorators, wrappers and monkey patching." description = "Module for decorators, wrappers and monkey patching."
category = "dev" category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "yarl"
version = "1.7.2"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6.2" python-versions = "^3.8"
content-hash = "404c2c3dc4953ada1f7211226d988da615dc3c227e2c4cf5ce0181f4cbe65aaa" content-hash = "d51c74b9a122a4a71f56722fa2d80c54f42152bf4c58ee4bc0da819f470acf89"
[metadata.files] [metadata.files]
aiohttp = [
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
]
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
]
astroid = [ astroid = [
{file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"},
{file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"},
] ]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
certifi = [ certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
] ]
charset-normalizer = [ charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
@ -207,13 +523,78 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
] ]
dill = [ dill = [
{file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"},
{file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"},
]
frozenlist = [
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
] ]
idna = [ idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
] ]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
@ -261,119 +642,297 @@ mccabe = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
] ]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
mypy = [
{file = "mypy-0.960-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3a3e525cd76c2c4f90f1449fd034ba21fcca68050ff7c8397bb7dd25dd8b8248"},
{file = "mypy-0.960-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7a76dc4f91e92db119b1be293892df8379b08fd31795bb44e0ff84256d34c251"},
{file = "mypy-0.960-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffdad80a92c100d1b0fe3d3cf1a4724136029a29afe8566404c0146747114382"},
{file = "mypy-0.960-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d390248ec07fa344b9f365e6ed9d205bd0205e485c555bed37c4235c868e9d5"},
{file = "mypy-0.960-cp310-cp310-win_amd64.whl", hash = "sha256:925aa84369a07846b7f3b8556ccade1f371aa554f2bd4fb31cb97a24b73b036e"},
{file = "mypy-0.960-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:239d6b2242d6c7f5822163ee082ef7a28ee02e7ac86c35593ef923796826a385"},
{file = "mypy-0.960-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f1ba54d440d4feee49d8768ea952137316d454b15301c44403db3f2cb51af024"},
{file = "mypy-0.960-cp36-cp36m-win_amd64.whl", hash = "sha256:cb7752b24528c118a7403ee955b6a578bfcf5879d5ee91790667c8ea511d2085"},
{file = "mypy-0.960-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:826a2917c275e2ee05b7c7b736c1e6549a35b7ea5a198ca457f8c2ebea2cbecf"},
{file = "mypy-0.960-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3eabcbd2525f295da322dff8175258f3fc4c3eb53f6d1929644ef4d99b92e72d"},
{file = "mypy-0.960-cp37-cp37m-win_amd64.whl", hash = "sha256:f47322796c412271f5aea48381a528a613f33e0a115452d03ae35d673e6064f8"},
{file = "mypy-0.960-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2c7f8bb9619290836a4e167e2ef1f2cf14d70e0bc36c04441e41487456561409"},
{file = "mypy-0.960-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbfb873cf2b8d8c3c513367febde932e061a5f73f762896826ba06391d932b2a"},
{file = "mypy-0.960-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc537885891382e08129d9862553b3d00d4be3eb15b8cae9e2466452f52b0117"},
{file = "mypy-0.960-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:481f98c6b24383188c928f33dd2f0776690807e12e9989dd0419edd5c74aa53b"},
{file = "mypy-0.960-cp38-cp38-win_amd64.whl", hash = "sha256:29dc94d9215c3eb80ac3c2ad29d0c22628accfb060348fd23d73abe3ace6c10d"},
{file = "mypy-0.960-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:33d53a232bb79057f33332dbbb6393e68acbcb776d2f571ba4b1d50a2c8ba873"},
{file = "mypy-0.960-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d645e9e7f7a5da3ec3bbcc314ebb9bb22c7ce39e70367830eb3c08d0140b9ce"},
{file = "mypy-0.960-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85cf2b14d32b61db24ade8ac9ae7691bdfc572a403e3cb8537da936e74713275"},
{file = "mypy-0.960-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a85a20b43fa69efc0b955eba1db435e2ffecb1ca695fe359768e0503b91ea89f"},
{file = "mypy-0.960-cp39-cp39-win_amd64.whl", hash = "sha256:0ebfb3f414204b98c06791af37a3a96772203da60636e2897408517fcfeee7a8"},
{file = "mypy-0.960-py3-none-any.whl", hash = "sha256:bfd4f6536bd384c27c392a8b8f790fd0ed5c0cf2f63fc2fed7bce56751d53026"},
{file = "mypy-0.960.tar.gz", hash = "sha256:d4fccf04c1acf750babd74252e0f2db6bd2ac3aa8fe960797d9f3ef41cf2bfd4"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
platformdirs = [ platformdirs = [
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
] ]
pylint = [ pylint = [
{file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"},
{file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"},
] ]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytest-asyncio = [
{file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"},
{file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"},
]
requests = [ requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
] ]
tomli = [ toml = [
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
] ]
typed-ast = [ tomli = [
{file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, ]
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, types-requests = [
{file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, {file = "types-requests-2.27.29.tar.gz", hash = "sha256:fb453b3a76a48eca66381cea8004feaaea12835e838196f5c7ac87c75c5c19ef"},
{file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, {file = "types_requests-2.27.29-py3-none-any.whl", hash = "sha256:014f4f82db7b96c41feea9adaea30e68cd64c230eeab34b70c29bebb26ec74ac"},
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, ]
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, types-toml = [
{file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, {file = "types-toml-0.10.7.tar.gz", hash = "sha256:a567fe2614b177d537ad99a661adc9bfc8c55a46f95e66370a4ed2dd171335f9"},
{file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, {file = "types_toml-0.10.7-py3-none-any.whl", hash = "sha256:05a8da4bfde2f1ee60e90c7071c063b461f74c63a9c3c1099470c08d6fa58615"},
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, ]
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, types-urllib3 = [
{file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, {file = "types-urllib3-1.26.15.tar.gz", hash = "sha256:c89283541ef92e344b7f59f83ea9b5a295b16366ceee3f25ecfc5593c79f794e"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, {file = "types_urllib3-1.26.15-py3-none-any.whl", hash = "sha256:6011befa13f901fc934f59bb1fd6973be6f3acf4ebfce427593a27e7f492918f"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
{file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
{file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
{file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
] ]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
] ]
urllib3 = [ urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
] ]
wrapt = [ wrapt = [
{file = "wrapt-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7"}, {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
{file = "wrapt-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
{file = "wrapt-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
{file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
{file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
{file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
{file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
{file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
{file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
{file = "wrapt-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330"}, {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
{file = "wrapt-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c"}, {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
{file = "wrapt-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
{file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
{file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
{file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
{file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
{file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.0-cp310-cp310-win32.whl", hash = "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff"}, {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d"}, {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
{file = "wrapt-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
{file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
{file = "wrapt-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0"}, {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
{file = "wrapt-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425"}, {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
{file = "wrapt-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48"}, {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
{file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
{file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
{file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
{file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
{file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
{file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
{file = "wrapt-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8"}, {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
{file = "wrapt-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06"}, {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
{file = "wrapt-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4"}, {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
{file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
{file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
{file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
{file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
{file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
{file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
{file = "wrapt-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68"}, {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
{file = "wrapt-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3"}, {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
{file = "wrapt-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d"}, {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
{file = "wrapt-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38"}, {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
{file = "wrapt-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
{file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
{file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
{file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
{file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
{file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
{file = "wrapt-1.14.0-cp38-cp38-win32.whl", hash = "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f"}, {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
{file = "wrapt-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e"}, {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
{file = "wrapt-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1"}, {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
{file = "wrapt-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4"}, {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
{file = "wrapt-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
{file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
{file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
{file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
{file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
{file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
{file = "wrapt-1.14.0-cp39-cp39-win32.whl", hash = "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350"}, {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
{file = "wrapt-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc"}, {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
{file = "wrapt-1.14.0.tar.gz", hash = "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
] ]

View file

@ -1,16 +1,31 @@
[tool.poetry] [tool.poetry]
name = "kanidmradius" name = "kanidmradius"
version = "0.0.1" version = "0.0.1"
description = "" description = "FreeRADIUS Module for Kanidm Authentication"
authors = [] authors = [
"James Hodgkinson <james@terminaloutcomes.com>"
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6.2" python = "^3.8"
requests = "^2.27.1" requests = "^2.27.1"
toml = "^0.10.2"
aiohttp = "^3.8.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pylint = "^2.13.9" pylint = "^2.13.9"
mypy = "^0.960"
types-requests = "^2.27.29"
pytest = "^7.1.2"
types-toml = "^0.10.7"
pytest-asyncio = "^0.18.3"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.pylint.MASTER]
disable="W0511"
[tool.pytest.ini_options]
asyncio_mode = "auto"

View file

@ -0,0 +1,24 @@
#!/bin/bash
if [ -z "${IMAGE}" ]; then
IMAGE="kanidm/radius:devel"
fi
echo "Running docker container: ${IMAGE}"
if [ -z "${CONFIG_FILE}" ]; then
CONFIG_FILE="$(pwd)/../examples/kanidm"
fi
echo "Using config file: ${CONFIG_FILE}"
if [ ! -d "/tmp/kanidm/" ]; then
echo "Can't find /tmp/kanidm - you might need to run insecure_generate_certs.sh"
fi
echo "Starting the dev container..."
#shellcheck disable=SC2068
docker run --rm -it \
--network host \
--name radiusd \
-v /tmp/kanidm/:/etc/raddb/certs/ \
-v "${CONFIG_FILE}:/data/kanidm" \
${IMAGE} $@

View file

@ -306,7 +306,7 @@ authorize {
# attributes in the request, and turning them into attributes # attributes in the request, and turning them into attributes
# which are more standard. # which are more standard.
# #
# It takes care of processing the 'raddb/mods-config/preprocess/hints' # It takes care of processing the 'raddb/mods-config/preprocess/hints'
# and the 'raddb/mods-config/preprocess/huntgroups' files. # and the 'raddb/mods-config/preprocess/huntgroups' files.
preprocess preprocess
@ -353,7 +353,7 @@ authorize {
# the "wimax" module here means that it will fix the # the "wimax" module here means that it will fix the
# Calling-Station-Id attribute to the normal format as # Calling-Station-Id attribute to the normal format as
# specified in RFC 3580 Section 3.21 # specified in RFC 3580 Section 3.21
# wimax # wimax
# #
# Look for IPASS style 'realm/', and if not found, look for # Look for IPASS style 'realm/', and if not found, look for
@ -364,7 +364,7 @@ authorize {
# #
# Look for realms in user@domain format # Look for realms in user@domain format
suffix suffix
# ntdomain # ntdomain
# #
# This module takes care of EAP-MD5, EAP-TLS, and EAP-LEAP # This module takes care of EAP-MD5, EAP-TLS, and EAP-LEAP
@ -397,12 +397,12 @@ authorize {
# to read /etc/passwd or /etc/shadow directly, see the # to read /etc/passwd or /etc/shadow directly, see the
# mods-available/passwd module. # mods-available/passwd module.
# #
# unix # unix
# #
# Read the 'users' file. In v3, this is located in # Read the 'users' file. In v3, this is located in
# raddb/mods-config/files/authorize # raddb/mods-config/files/authorize
files # files
# #
# Look in an SQL database. The schema of the database # Look in an SQL database. The schema of the database
@ -415,7 +415,7 @@ authorize {
# If you are using /etc/smbpasswd, and are also doing # If you are using /etc/smbpasswd, and are also doing
# mschap authentication, the un-comment this line, and # mschap authentication, the un-comment this line, and
# configure the 'smbpasswd' module. # configure the 'smbpasswd' module.
# smbpasswd # smbpasswd
# #
# The ldap module reads passwords from the LDAP database. # The ldap module reads passwords from the LDAP database.
@ -434,7 +434,7 @@ authorize {
# #
# Enforce daily limits on time spent logged in. # Enforce daily limits on time spent logged in.
# daily # daily
# #
expiration expiration
@ -459,9 +459,9 @@ authorize {
# This permits you to do DB queries, for example. If the modules # This permits you to do DB queries, for example. If the modules
# listed here return "fail", then NO response is sent. # listed here return "fail", then NO response is sent.
# #
# Autz-Type Status-Server { # Autz-Type Status-Server {
# #
# } # }
} }
@ -528,7 +528,7 @@ authenticate {
# #
# Pluggable Authentication Modules. # Pluggable Authentication Modules.
# pam # pam
# Uncomment it if you want to use ldap for authentication # Uncomment it if you want to use ldap for authentication
# #
@ -541,9 +541,9 @@ authenticate {
# authentication server, and knows what to do with authentication. # authentication server, and knows what to do with authentication.
# LDAP servers do not. # LDAP servers do not.
# #
# Auth-Type LDAP { # Auth-Type LDAP {
# ldap # ldap
# } # }
# #
# Allow EAP authentication. # Allow EAP authentication.

View file

@ -90,7 +90,7 @@ authorize {
# #
# Look for realms in user@domain format # Look for realms in user@domain format
# #
# Note that proxying the inner tunnel authentication means # Note that proxying the inner tunnel authentication means
# that the user MAY use one identity in the outer session # that the user MAY use one identity in the outer session
# (e.g. "anonymous", and a different one here # (e.g. "anonymous", and a different one here
@ -344,21 +344,21 @@ post-auth {
# Instead of "use_tunneled_reply", change this "if (0)" to an # Instead of "use_tunneled_reply", change this "if (0)" to an
# "if (1)". # "if (1)".
# #
if (0) { #if (0) {
# #
# These attributes are for the inner-tunnel only, # These attributes are for the inner-tunnel only,
# and MUST NOT be copied to the outer reply. # and MUST NOT be copied to the outer reply.
# #
update reply { # update reply {
User-Name !* ANY # User-Name !* ANY
Message-Authenticator !* ANY # Message-Authenticator !* ANY
EAP-Message !* ANY # EAP-Message !* ANY
Proxy-State !* ANY # Proxy-State !* ANY
MS-MPPE-Encryption-Types !* ANY # MS-MPPE-Encryption-Types !* ANY
MS-MPPE-Encryption-Policy !* ANY # MS-MPPE-Encryption-Policy !* ANY
MS-MPPE-Send-Key !* ANY # MS-MPPE-Send-Key !* ANY
MS-MPPE-Recv-Key !* ANY # MS-MPPE-Recv-Key !* ANY
} # }
# #
# Copy the inner reply attributes to the outer # Copy the inner reply attributes to the outer
@ -366,10 +366,10 @@ post-auth {
# care of copying the outer session-state list to the # care of copying the outer session-state list to the
# outer reply. # outer reply.
# #
update { # update {
&outer.session-state: += &reply: # &outer.session-state: += &reply:
} # }
} #}
# #
# Access-Reject packets are sent through the REJECT sub-section of the # Access-Reject packets are sent through the REJECT sub-section of the

View file

@ -0,0 +1,29 @@
[kanidm_client]
url = "https://localhost:8443"
strict = false
ca = "/data/ca.crt"
user = "radius_service_account"
secret = "XRELDJUh2pk6RcxRzgScKLOAQd7hNk3RZHe73gFo8BM8D3Iq"
# default vlans for groups that don't specify one.
[DEFAULT]
vlan = 1
# [group.test]
# vlan =
[radiusd]
ca = "/data/certs/ca.pem"
key = '/data/certs/key.pem'
cert = "/data/certs/cert.pem"
dh = "/data/certs/dh"
required_group = "radius_access_allowed"
[client.localhost]
ipaddr = "127.0.0.1"
secret = "testing123"
[client.docker]
ipaddr = "172.17.0.0/16"
secret = "testing123"

View file

@ -0,0 +1,42 @@
""" tests the check_vlan function """
from typing import Any
import aiohttp
import pytest
from kanidm import KanidmClient
from kanidm.types import KanidmClientConfig
from kanidmradius import check_vlan
@pytest.mark.asyncio
async def test_check_vlan(event_loop: Any) -> None:
""" test 1 """
async with aiohttp.ClientSession(loop=event_loop) as session:
testconfig = KanidmClientConfig.parse_toml("""
uri='https://kanidm.example.com'
radius_groups = [
{ name = "crabz", "vlan" = 1234 },
{ name = "hello world", "vlan" = 12345 },
]
""")
print(f"{testconfig=}")
kanidm_client = KanidmClient(
config = testconfig,
session=session,
)
print(f"{kanidm_client.config=}")
assert check_vlan(
acc=12345678,
group={'name' : 'crabz'},
kanidm_client=kanidm_client
) == 1234
assert check_vlan(
acc=12345678,
group={'name' : 'foo'},
kanidm_client=kanidm_client
) == 12345678

View file

@ -0,0 +1,58 @@
""" tests the config file things """
from pathlib import Path
import sys
import toml
import pytest
from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config
EXAMPLE_CONFIG_FILE="../examples/config"
def test_load_config_file() -> None:
""" tests that the file loads """
if not Path(EXAMPLE_CONFIG_FILE).expanduser().resolve().exists():
print("Can't find client config file", file=sys.stderr)
pytest.skip()
config = load_config(EXAMPLE_CONFIG_FILE)
kanidm_config = KanidmClientConfig.parse_obj(config)
assert kanidm_config.uri == 'https://idm.example.com/'
print(f"{kanidm_config.uri=}")
print(kanidm_config)
def test_radius_groups() -> None:
""" testing loading a config file with radius groups defined """
config_toml = """
radius_groups = [
{ name = "hello world", "vlan" = 1234 },
]
"""
config_parsed = toml.loads(config_toml)
print(config_parsed)
kanidm_config = KanidmClientConfig.parse_obj(config_parsed)
for group in kanidm_config.radius_groups:
print(group.name)
assert group.name == "hello world"
def test_radius_clients() -> None:
""" testing loading a config file with radius groups defined """
config_toml = """
radius_clients = [ { name = "hello world", ipaddr = "10.0.0.5", secret = "cr4bj0oz" },
]
"""
config_parsed = toml.loads(config_toml)
print(config_parsed)
kanidm_config = KanidmClientConfig.parse_obj(config_parsed)
client = kanidm_config.radius_clients[0]
print(client.name)
assert client.name == "hello world"
assert client.ipaddr == "10.0.0.5"
assert client.secret == "cr4bj0oz"

View file

@ -69,8 +69,15 @@ impl AccountOpt {
.await; .await;
match rcred { match rcred {
Ok(Some(s)) => println!("Radius secret: {}", s), Ok(Some(s)) => println!(
Ok(None) => println!("NO Radius secret"), "RADIUS secret for {}: {}",
aopt.aopts.account_id.as_str(),
s,
),
Ok(None) => println!(
"No RADIUS secret set for user {}",
aopt.aopts.account_id.as_str(),
),
Err(e) => { Err(e) => {
error!("Error -> {:?}", e); error!("Error -> {:?}", e);
} }

View file

@ -78,7 +78,7 @@ impl GroupOpt {
.await .await
{ {
Err(e) => error!("Error -> {:?}", e), Err(e) => error!("Error -> {:?}", e),
Ok(_) => println!("Successfully added members to {}", gcopt.name.as_str()), Ok(_) => println!("Successfully added {:?} to group \"{}\"", &new_members, gcopt.name.as_str()),
} }
} }

View file

@ -16,8 +16,8 @@ RUN zypper install -y \
make automake autoconf \ make automake autoconf \
libopenssl-devel pam-devel \ libopenssl-devel pam-devel \
sqlite3-devel \ sqlite3-devel \
sccache && \ sccache
zypper clean -a RUN zypper clean -a
COPY . /usr/src/kanidm COPY . /usr/src/kanidm
@ -47,21 +47,26 @@ WORKDIR /usr/src/kanidm/kanidmd/daemon
ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld" ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld"
RUN if [ "${SCCACHE_REDIS}" != "" ]; \ RUN if [ "${SCCACHE_REDIS}" != "" ]; \
then \ then \
export CC="/usr/bin/sccache /usr/bin/clang" && \ export CARGO_INCREMENTAL=false && \
export CARGO_INCREMENTAL=false && \ export CC="/usr/bin/sccache /usr/bin/clang" && \
export RUSTC_WRAPPER=sccache && \ export RUSTC_WRAPPER=sccache && \
sccache --start-server; \ sccache --start-server; \
else \ else \
export CC="/usr/bin/clang"; \ export CC="/usr/bin/clang"; \
fi && \ fi
cargo build ${KANIDM_BUILD_OPTIONS} \
--features=${KANIDM_FEATURES} \ RUN if [ -z "${KANIDM_FEATURES}" ]; then \
--target-dir=/usr/src/kanidm/target/ \ cargo build -p daemon ${KANIDM_BUILD_OPTIONS} \
--release && \ --target-dir="/usr/src/kanidm/target/" \
if [ "${SCCACHE_REDIS}" != "" ]; \ --release; \
then sccache -s; \ else \
fi; cargo build -p daemon ${KANIDM_BUILD_OPTIONS} \
--target-dir="/usr/src/kanidm/target/" \
--features="${KANIDM_FEATURES}" \
--release; \
fi
RUN if [ "${SCCACHE_REDIS}" != "" ]; then sccache -s; fi
RUN ls -al /usr/src/kanidm/target/release RUN ls -al /usr/src/kanidm/target/release

View file

@ -47,3 +47,8 @@ The unix domain socket API is internal and will never be "stable".
The CLI is *not* an API and can change with the interest of human interaction during any release. The CLI is *not* an API and can change with the interest of human interaction during any release.
## Python module
The python module will typically trail changes in functionality of the core Rust code, and will be developed as we it for our own needs - please feel free to add functionality or improvements, or [ask for them in a Github issue](http://github.com/kanidm/kanidm/issues/new/choose)!
All code changes will include full type-casting wherever possible.

11
pykanidm/README.md Normal file
View file

@ -0,0 +1,11 @@
# kanidm
A Python module for interacting with Kanidm.
## Installation
This probably won't work until we package and release it...
```shell
python -m pip install kanidm
```

1
pykanidm/docs/README.md Symbolic link
View file

@ -0,0 +1 @@
../README.md

View file

@ -0,0 +1,3 @@
::: kanidm.KanidmClient

View file

@ -0,0 +1,3 @@
::: kanidm.types.KanidmClientConfig

View file

@ -0,0 +1,3 @@
::: kanidm.types.RadiusClient

326
pykanidm/kanidm/__init__.py Normal file
View file

@ -0,0 +1,326 @@
""" Kanidm python module """
from json import dumps, loads, JSONDecodeError
import logging
from pathlib import Path
import ssl
from typing import Any, Dict, Optional, Union
from pydantic import ValidationError
import aiohttp
from .exceptions import (
AuthBeginFailed,
AuthInitFailed,
AuthCredFailed,
AuthMechUnknown,
NoMatchingEntries,
)
from .types import (
AuthBeginResponse,
AuthStepPasswordResponse,
AuthInitResponse,
ClientResponse,
KanidmClientConfig,
)
from .utils import load_config
KANIDMURLS = {
"auth": "/v1/auth",
}
class KanidmClient:
"""Kanidm client module
config: a `KanidmClientConfig` object, if this is set, everything else is ignored
config_file: a `pathlib.Path` object pointing to a configuration file
uri: kanidm base URL
session: a `aiohttp.client.ClientSession`
verify_hostnames: verify the hostname is correct
verify_certificate: verify the validity of the certificate and its CA
ca_path: set this to a trusted CA certificate (PEM format)
"""
# pylint: disable=too-many-instance-attributes,too-many-arguments
def __init__(
self,
config: Optional[KanidmClientConfig] = None,
config_file: Optional[Union[Path, str]] = None,
uri: Optional[str] = None,
session: Optional[aiohttp.client.ClientSession] = None,
verify_hostnames: bool = True,
verify_certificate: bool = True,
ca_path: Optional[str] = None,
) -> None:
"""Constructor for KanidmClient"""
if config is not None:
self.config = config
else:
self.config = KanidmClientConfig(
uri=uri,
verify_hostnames=verify_hostnames,
verify_certificate=verify_certificate,
ca_path=ca_path,
)
if config_file is not None:
if not isinstance(config_file, Path):
config_file = Path(config_file)
config_data = load_config(config_file.expanduser().resolve())
self.config = self.config.parse_obj(config_data)
self.session = session
self.sessionid: Optional[str] = None
if self.config.uri is None:
raise ValueError("Please intitialize this with a server URI")
self._ssl: Optional[Union[bool, ssl.SSLContext]] = None
self._configure_ssl()
def _configure_ssl(self) -> None:
"""Sets up SSL configuration for the client"""
if self.config.verify_certificate is False:
self._ssl = False
else:
self._ssl = ssl.create_default_context(cafile=self.config.ca_path)
if self._ssl is not False:
# ignoring this for typing because mypy is being weird
# ssl.SSLContext.check_hostname is totally a thing
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname
self._ssl.check_hostname = self.config.verify_hostnames # type: ignore
def parse_config_data(
self,
config_data: Dict[str, Any],
) -> None:
"""hand it a config dict and it'll configure the client"""
try:
self.config.parse_obj(config_data)
except ValidationError as validation_error:
raise ValueError(f"Failed to validate configuration: {validation_error}")
def get_path_uri(self, path: str) -> str:
"""turns a path into a full URI"""
if path.startswith("/"):
path = path[1:]
return f"{self.config.uri}{path}"
# pylint: disable=too-many-arguments
async def _call(
self,
method: str,
path: str,
headers: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
json: Optional[Dict[str, str]] = None,
) -> ClientResponse:
if timeout is None:
timeout = self.config.connect_timeout
if self.session is None:
self.session = aiohttp.client.ClientSession()
async with self.session.request(
method=method,
url=self.get_path_uri(path),
headers=headers,
timeout=timeout,
json=json,
ssl=self._ssl,
) as request:
content = await request.content.read()
try:
response_json = loads(content)
if not isinstance(response_json, dict):
response_json = None
except JSONDecodeError as json_error:
logging.error("Failed to JSON Decode Response: %s", json_error)
response_json = {}
response_input = {
"data": response_json,
"content": content.decode("utf-8"),
"headers": request.headers,
"status_code": request.status,
}
logging.debug(dumps(response_input, default=str, indent=4))
response = ClientResponse.parse_obj(response_input)
return response
async def call_get(
self,
path: str,
headers: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
) -> ClientResponse:
"""does a get call to the server"""
return await self._call("GET", path, headers, timeout)
async def call_post(
self,
path: str,
headers: Optional[Dict[str, str]] = None,
json: Optional[Dict[str, Any]] = None,
timeout: Optional[int] = None,
) -> ClientResponse:
"""does a get call to the server"""
return await self._call(
method="POST", path=path, headers=headers, json=json, timeout=timeout
)
async def auth_init(self, username: str) -> AuthInitResponse:
"""init step, starts the auth session, sets the class-local session ID"""
init_auth = {"step": {"init": username}}
response = await self.call_post(
path=KANIDMURLS["auth"],
json=init_auth,
)
if response.status_code != 200:
logging.debug(
"Failed to authenticate, response from server: %s",
response.content,
)
# TODO: mock test this
raise AuthInitFailed(response.content)
if "x-kanidm-auth-session-id" not in response.headers:
logging.debug("response.content: %s", response.content)
logging.debug("response.headers: %s", response.headers)
raise ValueError(
f"Missing x-kanidm-auth-session-id header in init auth response: {response.headers}"
)
# TODO: setting the class-local session id, do we want this?
self.sessionid = response.headers["x-kanidm-auth-session-id"]
retval = AuthInitResponse.parse_obj(response.data)
retval.response = response
return retval
async def auth_begin(
self,
method: str = "password", # TODO: do we want a default auth mech to be set?
) -> ClientResponse:
"""the 'begin' step"""
begin_auth = {
"step": {
"begin": method,
}
}
response = await self.call_post(
KANIDMURLS["auth"],
json=begin_auth,
headers=self.session_header(),
)
if response.status_code != 200:
# TODO: write mocked test for this
raise AuthBeginFailed(response.content)
retobject = AuthBeginResponse.parse_obj(response.data)
retobject.response = response
return response
async def authenticate_password(
self,
username: Optional[str] = None,
password: Optional[str] = None,
) -> AuthStepPasswordResponse:
"""authenticates with a username and password, returns the auth token"""
if username is None and password is None:
if self.config.username is None or self.config.password is None:
raise ValueError(
"Need username/password to be in caller or class settings before calling authenticate_password"
)
username = self.config.username
password = self.config.password
if username is None or password is None:
raise ValueError("Username and Password need to be set somewhere!")
auth_init = await self.auth_init(username)
if len(auth_init.state.choose) == 0:
# there's no mechanisms at all - bail
# TODO: write test coverage for this
raise AuthMechUnknown(f"No auth mechanisms for {username}")
auth_begin = await self.auth_begin(
method="password",
)
# does a little bit of validation
auth_begin_object = AuthBeginResponse.parse_obj(auth_begin.data)
auth_begin_object.response = auth_begin
return await self.auth_step_password(password=password)
async def auth_step_password(
self,
password: Optional[str] = None,
) -> AuthStepPasswordResponse:
"""does the password auth step"""
if password is None:
password = self.config.password
if password is None:
raise ValueError(
"Password has to be passed to auth_step_password or in self.password!"
)
cred_auth = {"step": {"cred": {"password": password}}}
response = await self.call_post(
path="/v1/auth",
json=cred_auth,
)
if response.status_code != 200:
# TODO: write test coverage for this
logging.debug("Failed to authenticate, response: %s", response.content)
raise AuthCredFailed("Failed password authentication!")
result = AuthStepPasswordResponse.parse_obj(response.data)
result.response = response
print(f"auth_step_password: {result.dict()}")
# pull the token out and set it
if result.state.success is None:
# TODO: write test coverage for AuthCredFailed
raise AuthCredFailed
result.sessionid = result.state.success
return result
def session_header(
self,
sessionid: Optional[str] = None,
) -> Dict[str, str]:
"""create a headers dict from a session id"""
# TODO: perhaps allow session_header to take a dict and update it, too?
if sessionid is not None:
return {
"X-KANIDM-AUTH-SESSION-ID": sessionid,
}
if self.sessionid is not None:
return {
"X-KANIDM-AUTH-SESSION-ID": self.sessionid,
}
raise ValueError("Class doesn't have a sessionid stored and none was provided")
async def get_radius_token(
self, username: str, radius_session_id: str
) -> ClientResponse:
"""does the call to the radius token endpoint"""
path = f"/v1/account/{username}/_radius/_token"
headers = {
"Authorization": f"Bearer {radius_session_id}",
}
response = await self.call_get(
path,
headers,
)
if response.status_code == 404:
raise NoMatchingEntries(
f"No user found: '{username}' {response.headers['x-kanidm-opid']}"
)
return response

View file

@ -0,0 +1,25 @@
""" kanidm client exceptions """
class AuthBeginFailed(Exception):
"""Auth Failed at the begin step"""
class AuthCredFailed(Exception):
"""Auth Failed at the init step"""
class AuthInitFailed(Exception):
"""Auth Failed at the init step"""
class AuthMechUnknown(Exception):
"""Not sure what mech was passed but it wasn't the one we wanted"""
class ServerURLNotSet(Exception):
"""You haven't set the URL for the server!"""
class NoMatchingEntries(Exception):
"""user not found"""

0
pykanidm/kanidm/py.typed Normal file
View file

178
pykanidm/kanidm/types.py Normal file
View file

@ -0,0 +1,178 @@
""" type objects """
# pylint: disable=too-few-public-methods
from ipaddress import IPv4Address,IPv6Address, IPv6Network, IPv4Network
import socket
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field, validator
import toml
class ClientResponse(BaseModel):
"""response from an API call"""
content: Optional[str]
data: Optional[Dict[str, Any]]
headers: Dict[str, Any]
status_code: int
class AuthInitResponse(BaseModel):
"""Aelps parse the response from the Auth 'init' stage"""
class _AuthInitState(BaseModel):
"""sub-class for the AuthInitResponse model"""
# TODO: can we add validation for AuthInitResponse.state.choose?
choose: List[str]
sessionid: str
state: _AuthInitState
response: Optional[ClientResponse]
class Config:
"""config class"""
arbitrary_types_allowed = True
class AuthBeginResponse(BaseModel):
"""Helps parse the response from the Auth 'begin' stage
"""
class _AuthBeginState(BaseModel):
"""Helps parse the response from the Auth 'begin' stage
'continue' had to be renamed 'continue_list'
because 'continue' is a reserved python term
"""
continue_list: List[str] = Field(..., title="continue", alias="continue")
# TODO: can we add validation for AuthBeginResponse.state.continue_list?
sessionid: str
state: _AuthBeginState
response: Optional[ClientResponse]
class Config:
"""config class"""
arbitrary_types_allowed = True
class AuthStepPasswordResponse(BaseModel):
"""helps parse the response from the auth 'password' stage"""
class _AuthStepPasswordState(BaseModel):
"""subclass to help parse the response from the auth 'step password' stage"""
success: Optional[str]
sessionid: str
state: _AuthStepPasswordState
response: Optional[ClientResponse]
class Config:
"""config class"""
arbitrary_types_allowed = True
class RadiusGroup(BaseModel):
"""group for kanidm radius"""
name: str
vlan: int
@validator("vlan")
def validate_vlan(cls, value: int) -> int:
"""validate the vlan option is above 0"""
if not value > 0:
raise ValueError(f"VLAN setting has to be above 0! Got: {value}")
return value
class RadiusClient(BaseModel):
"""Client config for Kanidm FreeRADIUS integration,
this is a pydantic model.
name: (str) An identifier for the client definition
ipaddr: (str) A single IP Address, CIDR or
DNS hostname (which will be resolved on startup,
preferring A records over AAAA).
FreeRADIUS doesn't recommend using DNS.
secret: (str) The password the client should use to
authenticate.
"""
name: str
ipaddr: str
secret: str
@validator("ipaddr")
def validate_ipaddr(cls, value: str) -> str:
"""validates the ipaddr field is an IP address, CIDR or valid hostname"""
for typedef in (IPv6Network, IPv6Address, IPv4Address, IPv4Network):
try:
typedef(value)
return value
except ValueError:
pass
try:
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}")
class KanidmClientConfig(BaseModel):
"""Configuration file definition for Kanidm client config
Based on struct KanidmClientConfig in kanidm_client/src/lib.rs
See source code for fields
"""
uri: Optional[str] = None
verify_hostnames: bool = True
verify_certificate: bool = True
ca_path: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
radius_cert_path: str = "/etc/raddb/certs/cert.pem"
radius_key_path: str = "/etc/raddb/certs/key.pem" # the signing key for radius TLS
radius_dh_path: str = "/etc/raddb/certs/dh.pem" # the diffie-hellman output
radius_ca_path: str = "/etc/raddb/certs/ca.pem" # the diffie-hellman output
radius_required_groups: List[str] = []
radius_default_vlan: int = 1
radius_groups: List[RadiusGroup] = []
radius_clients: List[RadiusClient] = []
connect_timeout: int = 30
@classmethod
def parse_toml(cls, input_string: str) -> Any:
"""loads from a string"""
return super().parse_obj(toml.loads(input_string))
@validator("uri")
def validate_uri(cls, value: Optional[str]) -> Optional[str]:
"""validator for the uri field"""
if value is not None:
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}"
)
# make sure the URI ends with a /
if not value.endswith("/"):
value = f"{value}/"
return value

21
pykanidm/kanidm/utils.py Normal file
View file

@ -0,0 +1,21 @@
""" utility functions """
from pathlib import Path
from typing import Any, Dict, Union
import toml
def load_config(filename: Union[str, Path] = "/etc/kanidm/config") -> Dict[str, Any]:
"""loads the configuration file"""
if isinstance(filename, Path):
config_filepath = filename
else:
config_filepath = Path(filename).expanduser().resolve()
if not config_filepath.exists():
raise FileNotFoundError(
f"Failed to find configuration file ({config_filepath}), quitting!",
)
config_data: Dict[str, Any] = toml.load(config_filepath.open(encoding="utf-8"))
return config_data

25
pykanidm/mkdocs.yml Normal file
View file

@ -0,0 +1,25 @@
# mkdocs.yml
site_name: kanidm python library
theme:
name: "material"
# site_url: https://kanidm.github.io/kanidm/master/pykanidm/
repo_name: 'kanidm/kanidm'
repo_url: 'https://github.com/kanidm/kanidm'
plugins:
- search:
- mkdocstrings:
default_handler: python
handlers:
python:
rendering:
show_source: true
watch:
- "kanidm/"
nav:
- "Home": README.md
- "KanidmClient": kanidmclient.md
- "KanidmClientConfig": kanidmclientconfig.md
- "RadiusClient": radiusclient.md

1619
pykanidm/poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

61
pykanidm/pyproject.toml Normal file
View file

@ -0,0 +1,61 @@
[tool.poetry]
name = "kanidm"
version = "0.0.1"
description = "Kanidm client library"
authors = [
"James Hodgkinson <james@terminaloutcomes.com>"
]
packages = [
{include = "kanidm"}
]
classifiers=[
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Operating System :: OS Independent",
]
[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.27.1"
toml = "^0.10.2"
pydantic = "^1.9.1"
aiohttp = "^3.8.1"
[tool.poetry.dev-dependencies]
pylint = "^2.13.9"
mypy = "^0.960"
types-requests = "^2.27.29"
pytest = "^7.1.2"
types-toml = "^0.10.7"
pylint-pydantic = "^0.1.4"
coverage = "^6.4.1"
pylint-pytest = "^1.1.2"
pytest-asyncio = "^0.18.3"
pytest-mock = "^3.7.0"
pytest-aiohttp = "^1.0.4"
black = "^22.3.0"
mkdocs = "^1.3.0"
mkdocs-material = "^8.3.4"
mkdocstrings = "^0.19.0"
mkdocstrings-python = "^0.7.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.pylint.MASTER]
max-line-length=150
disable="W0511,raise-missing-from"
extension-pkg-whitelist="pydantic"
# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422
load-plugins="pylint_pydantic,pylint_pytest"
[tool.pytest.ini_options]
asyncio_mode = "auto"
[tool.coverage.run]
source = ["kanidm"]
omit = ["tests"]

5
pykanidm/run_coverage.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
poetry run coverage run -m pytest -vvx && \
poetry run coverage html

View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE8DCCAtigAwIBAgIJAM28Wkrsl2exMA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMjAwBgNVBAMMKUJhZFNTTCBJbnRlcm1lZGlh
dGUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTE2MDgwODIxMTcwNVoXDTE4MDgw
ODIxMTcwNVowgagxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw
FAYDVQQHDA1TYW4gRnJhbmNpc2NvMTYwNAYDVQQKDC1CYWRTU0wgRmFsbGJhY2su
IFVua25vd24gc3ViZG9tYWluIG9yIG5vIFNOSS4xNDAyBgNVBAMMK2JhZHNzbC1m
YWxsYmFjay11bmtub3duLXN1YmRvbWFpbi1vci1uby1zbmkwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDCBOz4jO4EwrPYUNVwWMyTGOtcqGhJsCK1+ZWe
sSssdj5swEtgTEzqsrTAD4C2sPlyyYYC+VxBXRMrf3HES7zplC5QN6ZnHGGM9kFC
xUbTFocnn3TrCp0RUiYhc2yETHlV5NFr6AY9SBVSrbMo26r/bv9glUp3aznxJNEx
tt1NwMT8U7ltQq21fP6u9RXSM0jnInHHwhR6bCjqN0rf6my1crR+WqIW3GmxV0Tb
ChKr3sMPR3RcQSLhmvkbk+atIgYpLrG6SRwMJ56j+4v3QHIArJII2YxXhFOBBcvm
/mtUmEAnhccQu3Nw72kYQQdFVXz5ZD89LMOpfOuTGkyG0cqFAgMBAAGjRTBDMAkG
A1UdEwQCMAAwNgYDVR0RBC8wLYIrYmFkc3NsLWZhbGxiYWNrLXVua25vd24tc3Vi
ZG9tYWluLW9yLW5vLXNuaTANBgkqhkiG9w0BAQsFAAOCAgEAsuFs0K86D2IB20nB
QNb+4vs2Z6kECmVUuD0vEUBR/dovFE4PfzTr6uUwRoRdjToewx9VCwvTL7toq3dd
oOwHakRjoxvq+lKvPq+0FMTlKYRjOL6Cq3wZNcsyiTYr7odyKbZs383rEBbcNu0N
c666/ozs4y4W7ufeMFrKak9UenrrPlUe0nrEHV3IMSF32iV85nXm95f7aLFvM6Lm
EzAGgWopuRqD+J0QEt3WNODWqBSZ9EYyx9l2l+KI1QcMalG20QXuxDNHmTEzMaCj
4Zl8k0szexR8rbcQEgJ9J+izxsecLRVp70siGEYDkhq0DgIDOjmmu8ath4yznX6A
pYEGtYTDUxIvsWxwkraBBJAfVxkp2OSg7DiZEVlMM8QxbSeLCz+63kE/d5iJfqde
cGqX7rKEsVW4VLfHPF8sfCyXVi5sWrXrDvJm3zx2b3XToU7EbNONO1C85NsUOWy4
JccoiguV8V6C723IgzkSgJMlpblJ6FVxC6ZX5XJ0ZsMI9TIjibM2L1Z9DkWRCT6D
QjuKbYUeURhScofQBiIx73V7VXnFoc1qHAUd/pGhfkCUnUcuBV1SzCEhjiwjnVKx
HJKvc9OYjJD0ZuvZw9gBrY7qKyBX8g+sglEGFNhruH8/OhqrV8pBXX/EWY0fUZTh
iywmc6GTT7X94Ze2F7iB45jh7WQ=
-----END CERTIFICATE-----

View file

@ -0,0 +1,149 @@
""" testing auth things """
import logging
import os
import aiohttp
import pytest
from pytest_mock import MockerFixture
# pylint: disable=unused-import
from testutils import client, client_configfile, MockResponse
from kanidm import KanidmClient
from kanidm.exceptions import AuthCredFailed, AuthInitFailed
from kanidm.types import AuthBeginResponse
logging.basicConfig(level=logging.DEBUG)
@pytest.mark.asyncio
async def test_auth_init(client_configfile: KanidmClient) -> None:
"""tests the auth init step"""
print("Starting client...")
print(f"Doing auth_init for {client_configfile.config.username}")
if client_configfile.config.username is None:
raise ValueError("This path shouldn't be possible in the test!")
async with aiohttp.ClientSession() as session:
client_configfile.session = session
result = await client_configfile.auth_init(client_configfile.config.username)
print(f"{result=}")
print(result.dict())
assert result.sessionid
@pytest.mark.asyncio
async def test_auth_begin(client_configfile: KanidmClient) -> None:
"""tests the auth begin step"""
print(f"Doing auth_init for {client_configfile.config.username}")
async with aiohttp.ClientSession() as session:
client_configfile.session = session
if client_configfile.config.username is None:
raise ValueError("This path shouldn't be possible in the test!")
result = await client_configfile.auth_init(client_configfile.config.username)
print(f"{result=}")
print("Result dict:")
print(result.dict())
assert result.sessionid
print(f"Doing auth_begin for {client_configfile.config.username}")
begin_result = await client_configfile.auth_begin(
# username=client.username,
method="password",
)
print(f"{begin_result=}")
print(begin_result.data)
retval = begin_result.data
if retval is None:
raise pytest.fail("Failed to do begin_result")
retval["response"] = begin_result
assert AuthBeginResponse.parse_obj(retval)
@pytest.mark.asyncio
async def test_authenticate_flow(client_configfile: KanidmClient) -> None:
"""tests the authenticate() flow"""
async with aiohttp.ClientSession() as session:
print(f"Doing client.authenticate for {client_configfile.config.username}")
client_configfile.session = session
result = await client_configfile.authenticate_password()
print(result)
@pytest.mark.asyncio
async def test_authenticate_flow_fail(client_configfile: KanidmClient) -> None:
"""tests the authenticate() flow with a valid (hopefully) usernamd and invalid password"""
if not bool(os.getenv("RUN_SCARY_TESTS", None)):
pytest.skip(reason="Skipping because env var RUN_SCARY_TESTS isn't set")
print("Starting client...")
if (
client_configfile.config.uri is None
or client_configfile.config.username is None
or client_configfile.config.password is None
):
pytest.skip("Please ensure you have a username, password and uri in the config")
print(f"Doing client.authenticate for {client_configfile.config.username}")
async with aiohttp.ClientSession() as session:
client_configfile.session = session
with pytest.raises((AuthCredFailed, AuthInitFailed)):
result = await client_configfile.authenticate_password(
username=client_configfile.config.username,
password="cheese",
)
print(result)
# TODO: mock a call to auth_init when a 200 response is not returned, raises AuthInitFailed
# TODO: mock a call to auth_init when "x-kanidm-auth-session-id" not in response.headers, raises ValueError
# TODO: mock a call to auth_begin when a 200 response is not returned, raises AuthBeginFailed
# TODO: mock a call to auth_step_password when a 200 response is not returned, raises AuthCredFailed
@pytest.mark.asyncio
async def test_authenticate_inputs_validation(
client: KanidmClient, mocker: MockerFixture
) -> None:
"""tests if you pass username but not password and password but not username"""
resp = MockResponse("crabs are cool", 200)
mocker.patch("aiohttp.ClientSession.post", return_value=resp)
async with aiohttp.ClientSession() as session:
client.session = session
with pytest.raises(ValueError):
await client.authenticate_password(username="cheese")
with pytest.raises(ValueError):
await client.authenticate_password(password="cheese")
client.config.password = None
client.config.username = "crabby"
with pytest.raises(ValueError):
await client.authenticate_password()
client.config.password = "cR4bzR0ol"
client.config.username = None
with pytest.raises(ValueError):
await client.authenticate_password()
client.config.username = None
client.config.password = None
with pytest.raises(ValueError):
await client.authenticate_password()
@pytest.mark.asyncio
async def test_auth_step_password(client: KanidmClient) -> None:
"""tests things"""
with pytest.raises(ValueError):
async with aiohttp.ClientSession() as session:
client.session = session
await client.auth_step_password()

View file

@ -0,0 +1,125 @@
""" tests the config file things """
import logging
from pathlib import Path
import sys
import aiohttp
import pydantic
import pytest
from kanidm import KanidmClient
from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config
logging.basicConfig(level=logging.DEBUG)
EXAMPLE_CONFIG_FILE = "../examples/config"
@pytest.fixture(scope="function")
async def client() -> KanidmClient:
"""sets up a client with a basic thing"""
async with aiohttp.ClientSession() as session:
return KanidmClient(
uri="https://idm.example.com",
session=session,
)
def test_load_config_file() -> None:
"""tests that the file loads"""
if not Path(EXAMPLE_CONFIG_FILE).expanduser().resolve().exists():
print("Can't find client config file", file=sys.stderr)
pytest.skip()
print("Loading config file")
config = load_config(EXAMPLE_CONFIG_FILE)
assert config.get("uri") == "https://idm.example.com"
print(f"{config.get('uri')=}")
print(config)
def test_load_missing_config_file() -> None:
"""tests that an error is raised"""
with pytest.raises(
FileNotFoundError,
match=EXAMPLE_CONFIG_FILE + "cheese",
):
load_config(EXAMPLE_CONFIG_FILE + "cheese")
def test_parse_config_validationerror(client: KanidmClient) -> None:
"""tests parse_config with a faulty input"""
testdict = {"verify_certificate": "that was weird."}
with pytest.raises(ValueError):
client.parse_config_data(config_data=testdict)
@pytest.mark.asyncio
async def test_parse_config_data(client: KanidmClient) -> None:
"""tests parse_config witha valid input"""
async with aiohttp.ClientSession() as session:
client.session = session
testdict = {
"uri": "https://example.com",
"username": "testuser",
"password": "CraBzR0oL",
}
client.parse_config_data(config_data=testdict)
@pytest.mark.asyncio
async def test_init_with_uri() -> None:
"""tests the class"""
async with aiohttp.ClientSession() as session:
testclient = KanidmClient(
uri="https://example.com",
session=session,
)
assert testclient.config.uri == "https://example.com/"
@pytest.mark.asyncio
async def test_init_with_session() -> None:
"""tests the class"""
async with aiohttp.ClientSession() as session:
testclient = KanidmClient(
uri="https://google.com",
session=session,
)
assert testclient.session is session
def test_config_invalid_uri() -> None:
"""tests passing an invalid uri to the config parser"""
test_input = {
"uri": "asdfsadfasd",
}
with pytest.raises(pydantic.ValidationError):
KanidmClientConfig.parse_obj(test_input)
def test_config_none_uri() -> None:
"""tests passing an invalid uri to the config parser"""
with pytest.raises(ValueError, match="Please intitialize this with a server URI"):
KanidmClient(uri=None)
def test_config_loader_str() -> None:
"""tests passing an invalid uri to the config parser"""
with pytest.raises(FileNotFoundError):
KanidmClient(config_file="hello world")
def test_config_init() -> None:
"""tests passing config object"""
config = KanidmClientConfig(uri="https://idp.crabzrool.test")
assert KanidmClient(config=config)

View file

@ -0,0 +1,49 @@
""" testing get_radius_token """
import json
import logging
import os
# from typing import Any
import aiohttp
import pytest
# from pytest_mock import MockerFixture
# pylint: disable=unused-import
from testutils import client, client_configfile
from kanidm import KanidmClient
# from kanidm.exceptions import AuthCredFailed, AuthInitFailed
# from kanidm.types import AuthBeginResponse
logging.basicConfig(level=logging.DEBUG)
@pytest.mark.asyncio
async def test_radius_call(client_configfile: KanidmClient) -> None:
"""tests the radius call step"""
print(f"Doing auth_init for {client_configfile.config.username}")
if "RADIUS_USER" not in os.environ:
pytest.skip(
"Skipping this test - set RADIUS_USER environment variable to a valid RADIUS user."
)
radius_user = os.environ["RADIUS_USER"]
if client_configfile.config.username is None:
raise ValueError("This path shouldn't be possible in the test!")
async with aiohttp.ClientSession() as session:
client_configfile.session = session
radius_session = await client_configfile.authenticate_password()
result = await client_configfile.get_radius_token(
radius_user, radius_session_id=radius_session.sessionid
)
print(f"{result=}")
print(json.dumps(result.dict(), indent=4, default=str))

View file

@ -0,0 +1,36 @@
""" testing session header function """
import pytest
import aiohttp.client_exceptions
from testutils import client
from kanidm import KanidmClient
def test_session_header(client: KanidmClient) -> None:
"""tests the session_header function"""
with pytest.raises(ValueError):
client.session_header()
assert client.session_header("testval") == {
"X-KANIDM-AUTH-SESSION-ID": "testval",
}
@pytest.mark.asyncio
async def test_session_creator(client: KanidmClient) -> None:
"""tests the session_header function"""
client.session = None
client.config.uri = "🦀"
with pytest.raises(aiohttp.client_exceptions.InvalidURL):
await client._call(method="GET", path="/") # pylint: disable=protected-access
# pytest.raises(ValueError):
# client.session_header()
# assert client.session_header("testval") == {
# "X-KANIDM-AUTH-SESSION-ID": "testval",
# }

View file

@ -0,0 +1,201 @@
""" tests ssl validation and CA setting etc """
from pathlib import Path
import aiohttp
import aiohttp.client_exceptions
import pytest
from kanidm import KanidmClient
@pytest.mark.asyncio
async def test_ssl_valid() -> None:
"""tests a valid connection"""
url = "https://badssl.com"
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri=url,
session=session,
)
result = await client.call_get("/")
assert result.content
print(f"{result.status_code=}")
@pytest.mark.asyncio
async def test_ssl_self_signed() -> None:
"""tests with a self-signed cert"""
url = "https://self-signed.badssl.com"
async with aiohttp.ClientSession() as session:
print("testing self signed cert with defaults and expecting an error")
client = KanidmClient(
uri=url,
session=session,
)
with pytest.raises(aiohttp.client_exceptions.ClientConnectorCertificateError):
await client.call_get("/")
@pytest.mark.asyncio
async def test_ssl_self_signed_with_verify() -> None:
"""tests with a self-signed cert"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://self-signed.badssl.com",
session=session,
verify_certificate=False,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_self_signed_no_verify_certificate() -> None:
"""tests with a self-signed cert"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://self-signed.badssl.com",
session=session,
verify_certificate=False,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_wrong_hostname_throws_error() -> None:
"""tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://wrong.host.badssl.com/", session=session, verify_hostnames=True
)
with pytest.raises(
aiohttp.client_exceptions.ClientConnectorCertificateError,
match="Cannot connect to host wrong.host.badssl.com:443",
):
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_wrong_hostname_dont_verify_hostnames() -> None:
"""tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://wrong.host.badssl.com/",
session=session,
verify_hostnames=False,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_wrong_hostname_verify_certificate() -> None:
"""tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://wrong.host.badssl.com/",
session=session,
verify_hostnames=False,
verify_certificate=False,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_revoked() -> None:
"""tests with a revoked certificate, it'll pass but one day this should be a thing"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://revoked.badssl.com/",
session=session,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_expired() -> None:
"""tests with an expired certificate"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://expired.badssl.com/",
session=session,
)
with pytest.raises(
aiohttp.client_exceptions.ClientConnectorCertificateError,
match="certificate verify failed: certificate has expired",
):
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_expired_ignore() -> None:
"""tests with an expired certificate"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://expired.badssl.com/",
session=session,
verify_certificate=False,
)
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_untrusted_root_throws() -> None:
"""tests with an untrusted root, which should throw an error"""
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://untrusted-root.badssl.com/",
session=session,
)
with pytest.raises(
aiohttp.client_exceptions.ClientConnectorCertificateError,
match="certificate verify failed: self signed certificate in certificate chain",
):
result = await client.call_get("/")
assert result.content
@pytest.mark.asyncio
async def test_ssl_untrusted_root_configured() -> None:
"""tests with an untrusted root, which should throw an error"""
testcert = Path("./tests/badssl_trusted_ca.pem").resolve()
if not testcert.exists():
pytest.skip(f"The trusted cert is missing from {testcert}")
async with aiohttp.ClientSession() as session:
client = KanidmClient(
uri="https://untrusted-root.badssl.com/",
session=session,
ca_path=testcert.resolve().as_posix(),
)
with pytest.raises(
aiohttp.client_exceptions.ClientConnectorCertificateError,
match="certificate verify failed: self signed certificate in certificate chain",
):
result = await client.call_get("/")
assert result.content

View file

@ -0,0 +1,60 @@
""" tests types """
import pytest
import pydantic.error_wrappers
from kanidm.types import AuthInitResponse, KanidmClientConfig, RadiusGroup, RadiusClient
def test_auth_init_response() -> None:
"""tests AuthInitResponse"""
testobj = {
"sessionid": "crabzrool",
"state": {
"choose": ["passwordmfa"],
},
}
testval = AuthInitResponse.parse_obj(testobj)
assert testval.sessionid == "crabzrool"
def test_radiusgroup_vlan_negative() -> None:
"""tests RadiusGroup's vlan validator"""
with pytest.raises(pydantic.error_wrappers.ValidationError):
RadiusGroup(vlan=-1)
def test_radiusgroup_vlan_zero() -> None:
"""tests RadiusGroup's vlan validator"""
with pytest.raises(pydantic.error_wrappers.ValidationError):
RadiusGroup(vlan=0)
def test_radiusgroup_vlan_4096() -> None:
"""tests RadiusGroup's vlan validator"""
assert RadiusGroup(vlan=4096, name="crabzrool")
def test_radiusgroup_vlan_no_name() -> None:
"""tests RadiusGroup's vlan validator"""
with pytest.raises(
pydantic.error_wrappers.ValidationError, match="name\n.*field required"
):
RadiusGroup(
vlan=4096,
)
def test_kanidmconfig_parse_toml() -> None:
"""tests KanidmClientConfig.parse_toml()"""
config = KanidmClientConfig()
config.parse_toml("uri = 'https://crabzrool.example.com'")
def test_radius_client_bad_hostname() -> None:
"""tests with a bad hostname"""
with pytest.raises(pydantic.error_wrappers.ValidationError):
RadiusClient(name="test", ipaddr="thiscannotpossiblywork.kanidm.example.com",secret="nothing")
assert RadiusClient(name="test", ipaddr="kanidm.com",secret="nothing")

View file

@ -0,0 +1,45 @@
""" reusable widgets for testing """
from pathlib import Path
from typing import Any
import pytest
from kanidm import KanidmClient
@pytest.fixture(scope="function")
async def client() -> KanidmClient:
"""sets up a client with a basic thing"""
try:
return KanidmClient(uri="https://idm.example.com")
except FileNotFoundError:
raise pytest.skip("Couldn't find config file...")
@pytest.fixture(scope="function")
async def client_configfile() -> KanidmClient:
"""sets up a client from a config file"""
try:
return KanidmClient(config_file=Path("~/.config/kanidm"))
except FileNotFoundError:
raise pytest.skip("Couldn't find config file...")
class MockResponse:
"""mock the things"""
def __init__(self, text: str, status: int) -> None:
self._text = text
self.status = status
async def text(self) -> str:
"""mock the things"""
return self._text
# pylint: disable=invalid-name
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
"""mock the things"""
async def __aenter__(self) -> Any:
"""mock the things"""
return self