From 88e363501f43b6560ddeb68c995e3d3193eca7d3 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 1 Jun 2023 15:15:37 +0200 Subject: [PATCH 01/14] fix: Correctly set a WebAuthN key's last used timestamp Fixes #48, #56 --- kagi/views/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kagi/views/api.py b/kagi/views/api.py index e61505e..4f5d67a 100644 --- a/kagi/views/api.py +++ b/kagi/views/api.py @@ -189,7 +189,7 @@ def webauthn_verify_assertion(request): # Update counter. key.sign_count = sign_count - key.last_used = now() + key.last_used_at = now() key.save() try: From 3cb23f5b19a1e7ac1127367aa24bc9de7f620419 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 1 Jun 2023 15:32:55 +0200 Subject: [PATCH 02/14] feat: Added Django 4.2 and Python 3.11 support --- .github/workflows/main.yml | 12 +++++++++++- pyproject.toml | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c6da935..797a9f4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,14 +18,24 @@ jobs: - "3.8" - "3.9" - "3.10" + - "3.11" django-version: - "3.2" - "4.1" + - "4.2" exclude: - # Python 3.7 is compatible with Django < 4.0 + # Django 3.2 is compatible with Python <= 3.10 + - python-version: "3.11" + django-version: "3.2" + + # Django 4.1 is compatible with Python >= 3.8 - python-version: "3.7" django-version: "4.1" + # Django 4.2 is compatible with Python >= 3.8 + - python-version: "3.7" + django-version: "4.2" + steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index d45bbc4..2d25795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,18 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 2.2", "Framework :: Django :: 3.2", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Security", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries :: Python Modules", From 4a7f3a7b0fd5c5718f787fbc5c4b5de70097c418 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 1 Jun 2023 15:58:37 +0200 Subject: [PATCH 03/14] chore: Update invoke to 2.x for Python 3.11 support --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d25795..db31520 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ black = "^23.3" flake8 = "^5.0" Flake8-pyproject = "^1.2.3" furo = "2022.04.07" -invoke = "^1.3" +invoke = "^2.0" isort = "^5.11" livereload = "^2.6" psutil = {version = "^5.7", optional = true} From e05468b288e0b6907d8b8447c4e6f9b52f4eca27 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 1 Jun 2023 19:51:23 +0200 Subject: [PATCH 04/14] Upgrade all action versions to get rid of deprecation warnings. --- .github/workflows/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 797a9f4..68f1566 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,13 +37,13 @@ jobs: django-version: "4.2" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Set up Pip cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: pip-cache with: path: ~/.cache/pip @@ -53,7 +53,7 @@ jobs: - name: Install Poetry run: python -m pip install poetry - name: Set up Poetry cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: poetry-cache with: path: ~/.cache/pypoetry/virtualenvs @@ -79,13 +79,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.10" - name: Set Poetry cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: poetry-cache with: path: ~/.cache/pypoetry/virtualenvs From 7cb35838d2c391e835e9ec72fa8b71443ec175d9 Mon Sep 17 00:00:00 2001 From: Evan Ottinger Date: Sun, 4 Jun 2023 17:12:14 -0400 Subject: [PATCH 05/14] Replace SvgPathImage with SvgPathFillImage for accessibility in both light and dark mode --- kagi/views/totp_devices.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kagi/views/totp_devices.py b/kagi/views/totp_devices.py index c1d440d..8cb1b2b 100644 --- a/kagi/views/totp_devices.py +++ b/kagi/views/totp_devices.py @@ -15,7 +15,7 @@ from django.views.generic import FormView, ListView import qrcode -from qrcode.image.svg import SvgPathImage +from qrcode.image.svg import SvgPathFillImage from ..forms import TOTPForm from ..models import TOTPDevice @@ -43,7 +43,7 @@ def get_otpauth_url(self, key): ) def get_qrcode(self, data): - img = qrcode.make(data, image_factory=SvgPathImage) + img = qrcode.make(data, image_factory=SvgPathFillImage) buf = BytesIO() img.save(buf) return buf.getvalue().decode("utf-8") From f9053fc4e81bb8a2e6c70d30f8536d1bb1031db5 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 3 Jun 2023 11:03:08 +0200 Subject: [PATCH 06/14] Removed unneeded python classifiers. --- pyproject.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index db31520..f879345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,7 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", + # "Programming Language :: Python :: ..." is auto-generated by poetry! "Topic :: Security", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries :: Python Modules", From 9c15d2ccfd8e6fb91e2475e4e52392a508cb16c4 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 3 Jun 2023 11:26:02 +0200 Subject: [PATCH 07/14] Added support for trusted publishers. --- .github/workflows/main.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 68f1566..0743bbc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -109,27 +109,33 @@ jobs: runs-on: ubuntu-latest if: ${{ github.ref=='refs/heads/main' && github.event_name!='pull_request' }} + permissions: + contents: write + id-token: write + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + token: ${{ secrets.GH_TOKEN }} - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.8" - name: Check release id: check_release run: | python -m pip install --upgrade pip - python -m pip install poetry githubrelease httpx==0.16.1 autopub + python -m pip install poetry autopub[github] echo "##[set-output name=release;]$(autopub check)" - name: Publish if: ${{ steps.check_release.outputs.release=='' }} env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - git remote set-url origin https://$GITHUB_TOKEN@github.com/${{ github.repository }} autopub prepare - poetry build autopub commit + poetry build autopub githubrelease - poetry publish -u __token__ -p $PYPI_PASSWORD + - name: Upload package to PyPI + if: ${{ steps.check_release.outputs.release=='' }} + uses: pypa/gh-action-pypi-publish@release/v1 From e2b7d7b414e2061efc32be8d77a32198e138fa5c Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Wed, 7 Jun 2023 08:58:44 +0100 Subject: [PATCH 08/14] feat: Prevent secret submission from the client when adding TOTP devices (#72) Previously, a client could submit the secret for a TOTP device when adding it, through a hidden `base32_key` form field. With this commit, the secret is kept in a user session to remove a client's control over the secret. --- kagi/constants.py | 1 + kagi/templates/kagi/totp_device.html | 1 - kagi/tests/test_totp.py | 38 ++++++++++++++++++--- kagi/views/totp_devices.py | 50 ++++++++++++++++++---------- 4 files changed, 67 insertions(+), 23 deletions(-) create mode 100644 kagi/constants.py diff --git a/kagi/constants.py b/kagi/constants.py new file mode 100644 index 0000000..2ada318 --- /dev/null +++ b/kagi/constants.py @@ -0,0 +1 @@ +SESSION_TOTP_SECRET_KEY = "kagi_totp_secret" diff --git a/kagi/templates/kagi/totp_device.html b/kagi/templates/kagi/totp_device.html index 9c6f4c8..9d0bc61 100644 --- a/kagi/templates/kagi/totp_device.html +++ b/kagi/templates/kagi/totp_device.html @@ -21,7 +21,6 @@
{% csrf_token %} {{ form.as_p }} -
diff --git a/kagi/tests/test_totp.py b/kagi/tests/test_totp.py index e97f719..b67204c 100644 --- a/kagi/tests/test_totp.py +++ b/kagi/tests/test_totp.py @@ -29,7 +29,7 @@ def add_new_totp_device(client, *, url=None, now=None): base32_key = response.context_data["base32_key"] key = base64.b32decode(base32_key.encode("utf-8")) token = totp(key, now) - response = client.post(url, {"base32_key": base32_key, "token": token}) + response = client.post(url, {"token": token}) response.token = token return response @@ -61,20 +61,46 @@ def test_add_a_new_totp_device_context_data_contains_the_base32_key_and_otpauth_ ) -def test_add_a_new_totp_device_validates_the_otpauth_code_and_change_key_in_case_of_mismatch( +def test_add_a_new_totp_device_validates_the_otpauth_code_but_keeps_secret_in_case_of_mismatch( admin_client, ): response = admin_client.get(reverse("kagi:add-totp")) assert response.status_code == 200 base32_key = response.context_data["base32_key"] - response = admin_client.post( - reverse("kagi:add-totp"), {"base32_key": base32_key, "token": "123456"} - ) + + # Submit the form once, but with an invalid token. The secret should remain + # the same. + response = admin_client.post(reverse("kagi:add-totp"), {"token": "123456"}) assert response.status_code == 200 assert base32_key == response.context_data["base32_key"] form = response.context_data["form"] assert form.errors == {"token": ["That token is invalid."]} + # Submit the form a second time with the correct token. This should work. + key = base64.b32decode(base32_key.encode("utf-8")) + token = totp(key, timezone.now()) + response = admin_client.post(reverse("kagi:add-totp"), {"token": token}) + assert response.status_code == 302 + assert response.url == reverse("kagi:totp-devices") + # Ensure the secret is removed from the user session after adding a device. + assert "kagi_totp_secret" not in admin_client.session + + response = admin_client.get(reverse("kagi:totp-devices")) + assert len(response.context_data["totpdevice_list"]) == 1 + + +def test_add_a_new_totp_device_fails_when_secret_is_missing_in_session(admin_client): + response = admin_client.get(reverse("kagi:add-totp")) + assert response.status_code == 200 + + session = admin_client.session + del session["kagi_totp_secret"] + session.save() + + response = admin_client.post(reverse("kagi:add-totp"), {"token": "123456"}) + assert response.status_code == 302 + assert response.url == reverse("kagi:add-totp") + def test_add_a_new_totp_device_validates_the_otpauth_code_and_create_the_device_if_valid( admin_client, @@ -82,6 +108,8 @@ def test_add_a_new_totp_device_validates_the_otpauth_code_and_create_the_device_ response = add_new_totp_device(admin_client) assert response.status_code == 302 assert response.url == reverse("kagi:totp-devices") + # Ensure the secret is removed from the user session after adding a device. + assert "kagi_totp_secret" not in admin_client.session response = admin_client.get(reverse("kagi:totp-devices")) assert len(response.context_data["totpdevice_list"]) == 1 diff --git a/kagi/views/totp_devices.py b/kagi/views/totp_devices.py index 8cb1b2b..449ccca 100644 --- a/kagi/views/totp_devices.py +++ b/kagi/views/totp_devices.py @@ -7,9 +7,8 @@ from django.contrib import messages from django.contrib.sites.shortcuts import get_current_site from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy -from django.utils.functional import cached_property from django.utils.http import url_has_allowed_host_and_scheme, urlencode from django.utils.translation import gettext as _ from django.views.generic import FormView, ListView @@ -17,6 +16,7 @@ import qrcode from qrcode.image.svg import SvgPathFillImage +from ..constants import SESSION_TOTP_SECRET_KEY from ..forms import TOTPForm from ..models import TOTPDevice from .mixin import OriginMixin @@ -27,11 +27,33 @@ class AddTOTPDeviceView(OriginMixin, FormView): template_name = "kagi/totp_device.html" success_url = reverse_lazy("kagi:totp-devices") - def gen_key(self): - return os.urandom(20) - - def get_otpauth_url(self, key): - secret = b32encode(key) + def get(self, request, *args: str, **kwargs): + # When opening the view with a GET request, we treat it as a "add new + # device" request. There, we create a new TOTP secret and put it into + # the current user's session. Upon POST, the secret is read from the + # session again. + # Once a new TOTP device was successfully added, we'll drop the secret + # from the session. + # This approach allows to re-enter the token if mistyped, while keeping + # the same TOTP device setup on the TOTP generator. + self.secret = self.gen_secret() + request.session[SESSION_TOTP_SECRET_KEY] = self.secret + return super().get(request, *args, **kwargs) + + def post(self, request, *args: str, **kwargs): + # Try to get the TOTP secret from the session. If the secret doesn't + # exist, redirect to the view again, to configure a new TOTP secret. + self.secret = request.session.get(SESSION_TOTP_SECRET_KEY, None) + if not self.secret: + messages.error(request, _("Missing TOTP secret. Please try again.")) + return redirect(request.path) + + return super().post(request, *args, **kwargs) + + def gen_secret(self): + return b32encode(os.urandom(20)).decode() + + def get_otpauth_url(self, secret): issuer = get_current_site(self.request).name params = OrderedDict([("secret", secret), ("digits", 6), ("issuer", issuer)]) @@ -48,17 +70,10 @@ def get_qrcode(self, data): img.save(buf) return buf.getvalue().decode("utf-8") - @cached_property - def key(self): - try: - return b32decode(self.request.POST["base32_key"]) - except KeyError: - return self.gen_key() - def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - kwargs["base32_key"] = b32encode(self.key).decode() - kwargs["otpauth"] = self.get_otpauth_url(self.key) + kwargs["base32_key"] = self.secret + kwargs["otpauth"] = self.get_otpauth_url(self.secret) kwargs["qr_svg"] = self.get_qrcode(kwargs["otpauth"]) return kwargs @@ -70,8 +85,9 @@ def get_form_kwargs(self): return kwargs def form_valid(self, form): - device = TOTPDevice(user=self.request.user, key=self.key) + device = TOTPDevice(user=self.request.user, key=b32decode(self.secret)) if device.validate_token(form.cleaned_data["token"]): + del self.request.session[SESSION_TOTP_SECRET_KEY] device.save() messages.success(self.request, _("Device added.")) return super().form_valid(form) From 066a46aa10156db1956faf49693e1557e254dddf Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 8 Jun 2023 09:53:49 +0200 Subject: [PATCH 09/14] Prepare release (#76) Co-authored-by: Justin Mayer --- .github/workflows/main.yml | 12 ++++++------ RELEASE.md | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 RELEASE.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0743bbc..e0d00f4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,7 +117,7 @@ jobs: - uses: actions/checkout@v3 with: token: ${{ secrets.GH_TOKEN }} - - name: Setup Python + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.8" @@ -125,17 +125,17 @@ jobs: id: check_release run: | python -m pip install --upgrade pip - python -m pip install poetry autopub[github] - echo "##[set-output name=release;]$(autopub check)" + python -m pip install autopub[github] + autopub check - name: Publish - if: ${{ steps.check_release.outputs.release=='' }} + if: ${{ steps.check_release.outputs.autopub_release=='true' }} env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} run: | autopub prepare autopub commit - poetry build + autopub build autopub githubrelease - name: Upload package to PyPI - if: ${{ steps.check_release.outputs.release=='' }} + if: ${{ steps.check_release.outputs.autopub_release=='true' }} uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..69e68b5 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,8 @@ +Release type: minor + + * Add support for Python 3.11 and Django 4.2, by @MarkusH (#67). + * "Pin" primary keys to `AutoField` so no new migrations are generated for now (#55). + * Properly update `last_used_at` for FIDO tokens, by @MarkusH (#66). + * Improve secret submission security when adding TOTP devices, by @MarkusH (#72). + * Improve QR code display in Django Admin in dark mode, by @evanottinger (#75). + * Publish Kagi via PyPI trusted publisher system, by @apollo13 (#74). From 6b7935081038a355470218d46c86ce3eae126189 Mon Sep 17 00:00:00 2001 From: botpub Date: Thu, 8 Jun 2023 07:56:38 +0000 Subject: [PATCH 10/14] Release Kagi 0.4.0 --- CHANGELOG.md | 13 +++++++++++++ RELEASE.md | 8 -------- pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) delete mode 100644 RELEASE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0240f76..313f962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +0.4.0 - 2023-06-08 +------------------ + +* Add support for Python 3.11 and Django 4.2, by @MarkusH (#67). + * "Pin" primary keys to `AutoField` so no new migrations are generated for now (#55). + * Properly update `last_used_at` for FIDO tokens, by @MarkusH (#66). + * Improve secret submission security when adding TOTP devices, by @MarkusH (#72). + * Improve QR code display in Django Admin in dark mode, by @evanottinger (#75). + * Publish Kagi via PyPI trusted publisher system, by @apollo13 (#74). + +Contributed by [Florian Apolloner](https://github.com/apollo13) via [PR #76](https://github.com/justinmayer/kagi/pull/76/) + + 0.3.0 - 2022-09-18 ------------------ diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 69e68b5..0000000 --- a/RELEASE.md +++ /dev/null @@ -1,8 +0,0 @@ -Release type: minor - - * Add support for Python 3.11 and Django 4.2, by @MarkusH (#67). - * "Pin" primary keys to `AutoField` so no new migrations are generated for now (#55). - * Properly update `last_used_at` for FIDO tokens, by @MarkusH (#66). - * Improve secret submission security when adding TOTP devices, by @MarkusH (#72). - * Improve QR code display in Django Admin in dark mode, by @evanottinger (#75). - * Publish Kagi via PyPI trusted publisher system, by @apollo13 (#74). diff --git a/pyproject.toml b/pyproject.toml index f879345..9dd304e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "kagi" -version = "0.3.0" +version = "0.4.0" description = "Django app for WebAuthn and TOTP-based multi-factor authentication" authors = ["Justin Mayer ", "Rémy Hubscher "] license = "BSD-2-Clause" From 694d67d9127931614b6d39e7b5c00c27a8cd7a98 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 2 Jul 2023 18:32:19 +0200 Subject: [PATCH 11/14] Change botpub email address for Git commits --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9dd304e..ed5f4ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ Werkzeug = "^2.0" [tool.autopub] project-name = "Kagi" git-username = "botpub" -git-email = "botpub@autopub.rocks" +git-email = "52496925+botpub@users.noreply.github.com" append-github-contributor = true [tool.isort] From 0c88e00b7199fe1041600d3c5095a41b8e53b87c Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 25 Oct 2023 10:35:08 +0200 Subject: [PATCH 12/14] Update ReadTheDocs configuration --- .readthedocs.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 4094dc8..5e770a6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,19 @@ --- +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py +# Version of Python and requirements required to build the docs python: - version: 3.8 install: - requirements: docs/requirements.txt From 55ffccd7b5293853e8266567f9a080eda5f25055 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 25 Oct 2023 10:52:52 +0200 Subject: [PATCH 13/14] Upgrade OpenSSL to resolve Cryptography conflict --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ed5f4ae..b9dc921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ invoke = "^2.0" isort = "^5.11" livereload = "^2.6" psutil = {version = "^5.7", optional = true} -pyOpenSSL = "^22.0" +pyOpenSSL = "^23.0" pytest = "^7.1" pytest-cov = "^3.0" pytest-django = "^4.0" From b6cceb35f11b233121f3590b3c0b524c6322e9b0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 25 Oct 2023 09:31:43 +0200 Subject: [PATCH 14/14] Added README.rst link to RTD docs. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 0e48270..ff532a9 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,8 @@ Kagi is a relatively young project and has not yet been fully battle-tested. Its use in a high-impact environment should be accompanied by a thorough understanding of how it works before relying on it. +`Full documentation is hosted on Read the Docs`_. + Installation ------------ @@ -109,3 +111,4 @@ which served as useful initial scaffolding for this project. .. _Poetry: https://python-poetry.org/docs/#installation +.. _Full documentation is hosted on Read the Docs: https://kagi.readthedocs.io