mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
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:
parent
1072edbb5e
commit
805ac2dd16
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
|
@ -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:
|
||||||
|
|
105
.github/workflows/docker_build.yml
vendored
105
.github/workflows/docker_build.yml
vendored
|
@ -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/
|
|
45
.github/workflows/docker_build_kanidmd.yml
vendored
Normal file
45
.github/workflows/docker_build_kanidmd.yml
vendored
Normal 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
|
42
.github/workflows/docker_build_radiusd.yml
vendored
Normal file
42
.github/workflows/docker_build_radiusd.yml
vendored
Normal 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
23
.github/workflows/pykanidm_mypy.yml
vendored
Normal 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
22
.github/workflows/pykanidm_pylint.yml
vendored
Normal 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
28
.github/workflows/pykanidm_pytest.yml
vendored
Normal 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
12
.gitignore
vendored
|
@ -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
2
CODEOWNERS
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# yale-mistakes were made
|
||||||
|
/pykanidm/* @yaleman
|
68
Makefile
68
Makefile
|
@ -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
46
examples/kanidm
Normal 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
|
|
@ -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}"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
38
kanidm_book/src/developers/python.md
Normal file
38
kanidm_book/src/developers/python.md
Normal 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).
|
67
kanidm_book/src/developers/radius.md
Normal file
67
kanidm_book/src/developers/radius.md
Normal 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
|
||||||
|
```
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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" ]
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
199
kanidm_rlm_python/kanidmradius/__init__.py
Normal file
199
kanidm_rlm_python/kanidmradius/__init__.py
Normal 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)
|
39
kanidm_rlm_python/kanidmradius/radiusd.py
Normal file
39
kanidm_rlm_python/kanidmradius/radiusd.py
Normal 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
|
20
kanidm_rlm_python/kanidmradius/utils.py
Normal file
20
kanidm_rlm_python/kanidmradius/utils.py
Normal 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
|
|
@ -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,
|
|
@ -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
|
||||||
}
|
}
|
799
kanidm_rlm_python/poetry.lock
generated
799
kanidm_rlm_python/poetry.lock
generated
|
@ -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"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
24
kanidm_rlm_python/run_radius_container.sh
Executable file
24
kanidm_rlm_python/run_radius_container.sh
Executable 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} $@
|
|
@ -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.
|
|
@ -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
|
29
kanidm_rlm_python/test_data/kanidm_radius.toml
Normal file
29
kanidm_rlm_python/test_data/kanidm_radius.toml
Normal 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"
|
||||||
|
|
42
kanidm_rlm_python/tests/test_check_vlan.py
Normal file
42
kanidm_rlm_python/tests/test_check_vlan.py
Normal 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
|
58
kanidm_rlm_python/tests/test_config.py
Normal file
58
kanidm_rlm_python/tests/test_config.py
Normal 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"
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
11
pykanidm/README.md
Normal 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
1
pykanidm/docs/README.md
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../README.md
|
3
pykanidm/docs/kanidmclient.md
Normal file
3
pykanidm/docs/kanidmclient.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
::: kanidm.KanidmClient
|
3
pykanidm/docs/kanidmclientconfig.md
Normal file
3
pykanidm/docs/kanidmclientconfig.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
::: kanidm.types.KanidmClientConfig
|
3
pykanidm/docs/radiusclient.md
Normal file
3
pykanidm/docs/radiusclient.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
::: kanidm.types.RadiusClient
|
326
pykanidm/kanidm/__init__.py
Normal file
326
pykanidm/kanidm/__init__.py
Normal 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
|
25
pykanidm/kanidm/exceptions.py
Normal file
25
pykanidm/kanidm/exceptions.py
Normal 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
0
pykanidm/kanidm/py.typed
Normal file
178
pykanidm/kanidm/types.py
Normal file
178
pykanidm/kanidm/types.py
Normal 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
21
pykanidm/kanidm/utils.py
Normal 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
25
pykanidm/mkdocs.yml
Normal 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
1619
pykanidm/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
61
pykanidm/pyproject.toml
Normal file
61
pykanidm/pyproject.toml
Normal 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
5
pykanidm/run_coverage.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
poetry run coverage run -m pytest -vvx && \
|
||||||
|
poetry run coverage html
|
||||||
|
|
29
pykanidm/tests/badssl_trusted_ca.pem
Normal file
29
pykanidm/tests/badssl_trusted_ca.pem
Normal 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-----
|
149
pykanidm/tests/test_authenticate.py
Normal file
149
pykanidm/tests/test_authenticate.py
Normal 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()
|
125
pykanidm/tests/test_config_loader.py
Normal file
125
pykanidm/tests/test_config_loader.py
Normal 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)
|
49
pykanidm/tests/test_radius_token.py
Normal file
49
pykanidm/tests/test_radius_token.py
Normal 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))
|
36
pykanidm/tests/test_session_header.py
Normal file
36
pykanidm/tests/test_session_header.py
Normal 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",
|
||||||
|
# }
|
201
pykanidm/tests/test_ssl_ca.py
Normal file
201
pykanidm/tests/test_ssl_ca.py
Normal 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
|
60
pykanidm/tests/test_types.py
Normal file
60
pykanidm/tests/test_types.py
Normal 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")
|
45
pykanidm/tests/testutils.py
Normal file
45
pykanidm/tests/testutils.py
Normal 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
|
Loading…
Reference in a new issue