dnscms: log in to the CMS with Galtinn over OIDC

This commit is contained in:
ponas committed 2026-10-09 16:15:31 +02:00
1 parent 0689dcd4cd
commit de6b06b2ba
12 files changed
+962 -1

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Generated by Django 6.0.7 on 2026-08-04 17:43
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='GaltinnUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('galtinn_id', models.CharField(max_length=64, unique=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='galtinn_user', to=settings.AUTH_USER_MODEL)),
],
),
]
Whitespace-only changes.
+12
View File
@@ -0,0 +1,12 @@
from django.contrib.auth.models import User
from django.db import models
class GaltinnUser(models.Model):
"""Links a local user to its Galtinn identity"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="galtinn_user")
galtinn_id = models.CharField(max_length=64, unique=True)
def __str__(self):
return f"{self.user.username} (galtinn #{self.galtinn_id})"
+206
View File
@@ -0,0 +1,206 @@
"""
OIDC login via Galtinn.
Members of GALTINN_EDITOR_GROUP get editor access.
"""
import logging
import requests
from authlib.integrations.django_client import OAuth, OAuthError
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.auth import login
from django.contrib.auth.models import Group, User
from django.db import transaction
from django.db.models import Q
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from wagtail.admin.forms.account import NameEmailForm
from joserfc.errors import JoseError
from wagtail.admin.views.account import AccountView, NameEmailSettingsPanel
from dnscms.models import GaltinnUser
logger = logging.getLogger(__name__)
WAGTAIL_EDITORS_GROUP = "Editors" # stock Wagtail group, has admin access via wagtailcore
REQUIRED_CLAIMS = ("sub", "preferred_username", "email")
oauth = OAuth()
oauth.register(
"galtinn",
client_id=settings.OIDC_RP_CLIENT_ID,
client_secret=settings.OIDC_RP_CLIENT_SECRET,
server_metadata_url=f"{settings.GALTINN_BASE_URL}/api/oauth/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)
class LoginRefused(Exception):
pass
def is_enabled():
return bool(settings.OIDC_RP_CLIENT_ID and settings.GALTINN_EDITOR_GROUP)
def context(request):
return {"oidc_enabled": is_enabled()}
def _is_editor(claims):
group = settings.GALTINN_EDITOR_GROUP
return bool(group) and group in claims.get("groups", [])
def _create_user(claims):
username, email = claims["preferred_username"], claims["email"]
# an existing local user must be linked by hand, never matched on username or email
if User.objects.filter(Q(username__iexact=username) | Q(email__iexact=email)).exists():
logger.error(
"Galtinn user #%s (%r, %r) clashes with an unlinked local user; "
"refusing login, link manually",
claims["sub"],
username,
email,
)
raise LoginRefused("clash with unlinked local user")
editors = Group.objects.get(name=WAGTAIL_EDITORS_GROUP)
with transaction.atomic():
user = User.objects.create_user(
username=username,
email=email,
first_name=claims.get("given_name", ""),
last_name=claims.get("family_name", ""),
)
user.groups.add(editors)
GaltinnUser.objects.create(user=user, galtinn_id=claims["sub"])
logger.info("Created user %r from Galtinn login", username)
return user
def _sync_user(user, claims):
username = claims["preferred_username"]
if user.username != username:
if User.objects.exclude(pk=user.pk).filter(username__iexact=username).exists():
logger.error(
"Galtinn username %r maps to user #%s but another local user owns it; "
"refusing login, resolve manually",
username,
user.pk,
)
raise LoginRefused("username collision")
user.username = username
user.email = claims["email"]
user.first_name = claims.get("given_name", user.first_name)
user.last_name = claims.get("family_name", user.last_name)
user.is_active = True
# linked users log in via Galtinn only, so access follows the Galtinn group
if user.has_usable_password():
user.set_unusable_password()
user.save()
user.groups.add(Group.objects.get(name=WAGTAIL_EDITORS_GROUP))
return user
def _revoke(user):
user.groups.remove(*Group.objects.filter(name=WAGTAIL_EDITORS_GROUP))
if user.is_superuser:
logger.warning("Superuser %r is not in the Galtinn editor group", user.username)
return
user.is_active = False
user.save(update_fields=["is_active"])
logger.info("Deactivated %r: not in Galtinn editor group", user.username)
def resolve_user(claims):
"""Return the local user for these claims, or raise LoginRefused."""
missing = [name for name in REQUIRED_CLAIMS if not claims.get(name)]
if missing:
raise LoginRefused(f"missing claims: {', '.join(missing)}")
claims = {**claims, "sub": str(claims["sub"])}
# only the stored Galtinn ID identifies a user
user = User.objects.filter(galtinn_user__galtinn_id=claims["sub"]).first()
if user is None:
if not _is_editor(claims):
raise LoginRefused(f"unknown user #{claims['sub']} not in editor group")
return _create_user(claims)
if not _is_editor(claims):
_revoke(user)
raise LoginRefused("not in editor group")
return _sync_user(user, claims)
@admin.register(GaltinnUser)
class GaltinnUserAdmin(admin.ModelAdmin):
list_display = ["user", "galtinn_id"]
search_fields = ["user__username", "user__email", "galtinn_id"]
autocomplete_fields = ["user"]
class GaltinnNameEmailForm(NameEmailForm):
"""Read-only: name and email are overwritten from Galtinn claims on every login"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.disabled = True
class GaltinnNameEmailPanel(NameEmailSettingsPanel):
template_name = "wagtailadmin/account/settings_panels/name_email_galtinn.html"
form_class = GaltinnNameEmailForm
class GaltinnAccountView(AccountView):
"""Account view where Galtinn-linked users get a read-only name/email panel"""
def get_panels(self):
panels = super().get_panels()
if not GaltinnUser.objects.filter(user=self.request.user).exists():
return panels
return [
GaltinnNameEmailPanel(self.request, panel.user, panel.profile)
if panel.name == "name_email"
else panel
for panel in panels
]
def oidc_login(request):
if not is_enabled():
raise Http404
next_url = request.GET.get("next")
if next_url:
request.session["oidc_next"] = next_url
# from WAGTAIL_BASE_URL, not the request: the proxy strips https and the URI must match exactly
redirect_uri = settings.WAGTAIL_BASE_URL + reverse("oidc_callback")
return oauth.galtinn.authorize_redirect(request, redirect_uri)
def oidc_callback(request):
if not is_enabled():
raise Http404
try:
token = oauth.galtinn.authorize_access_token(request)
except (OAuthError, JoseError, requests.RequestException) as exc:
logger.warning("Galtinn OAuth flow failed: %s", exc)
messages.error(request, "Innlogging via Galtinn feilet. Prøv igjen.")
return redirect("wagtailadmin_login")
try:
user = resolve_user(token.get("userinfo") or {})
except LoginRefused as exc:
logger.warning("Galtinn login refused: %s", exc)
messages.error(
request, "Du har ikke tilgang til publiseringssystemet. Ta kontakt med edb@neuf.no."
)
return redirect("wagtailadmin_login")
login(request, user)
next_url = request.session.pop("oidc_next", "")
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
return redirect(next_url)
return redirect("wagtailadmin_home")
+8
View File
@@ -92,6 +92,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"dnscms.oidc.context",
],
},
},
@@ -204,6 +205,13 @@ FRONTEND_BASE_URL = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000")
# Unset means no webhooks are sent.
REVALIDATE_WEBHOOK_SECRET = os.environ.get("REVALIDATE_WEBHOOK_SECRET", "")
# OIDC login via Galtinn (see dnscms/oidc.py)
GALTINN_BASE_URL = os.environ.get("GALTINN_BASE_URL", "https://galtinn.neuf.no").rstrip("/")
# Galtinn group whose members get editor access. OIDC login needs this and the client ID.
GALTINN_EDITOR_GROUP = os.environ.get("GALTINN_EDITOR_GROUP", "")
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", "")
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", "")
WAGTAIL_HEADLESS_PREVIEW = {
"CLIENT_URLS": {"default": f"{FRONTEND_BASE_URL}/api/preview"},
"SERVE_BASE_URL": FRONTEND_BASE_URL,
@@ -0,0 +1,78 @@
<svg viewBox="0 0 196 285" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_2360)">
<path
d="M73.1191 228.849C72.7961 229.106 72.5445 229.738 72.3243 230.667C72.1642 231.336 72.0527 232.085 71.9069 232.791C71.5467 234.523 71.0035 236.235 70.1773 237.83C69.0623 239.991 67.5786 241.975 65.9004 243.787C64.2165 245.602 62.3411 247.248 60.3685 248.826C59.4021 249.606 58.4158 250.347 57.3952 251.176C57.1379 251.387 56.8806 251.604 56.6033 251.864L56.4947 251.973C56.4432 252.025 56.3889 252.079 56.3374 252.139C56.2345 252.253 56.1287 252.368 56.0429 252.496C55.8714 252.748 55.7199 253.019 55.6742 253.319C55.6427 253.465 55.6427 253.619 55.6341 253.771L55.657 254.205L55.6884 254.688C55.7085 255.011 55.7342 255.334 55.7628 255.655C55.9858 258.224 56.3774 260.757 56.8377 263.258C57.298 265.759 57.8355 268.234 58.3615 270.692C58.6245 271.921 58.8847 273.145 59.122 274.362C59.3564 275.577 59.5737 276.795 59.7023 277.975C59.7652 278.561 59.8052 279.147 59.791 279.682C59.7852 279.95 59.7624 280.199 59.7252 280.416C59.708 280.525 59.6823 280.619 59.6594 280.702C59.6451 280.736 59.6309 280.774 59.6223 280.805L59.608 280.831C59.5994 280.848 59.5823 280.848 59.5708 280.856C59.5622 280.856 59.5565 280.856 59.5479 280.856H59.5222C59.4851 280.845 59.4507 280.842 59.4136 280.836C59.2478 280.805 59.0333 280.734 58.8075 280.642C58.6931 280.588 58.5788 280.542 58.4587 280.482L58.2815 280.391L58.2357 280.368C58.2357 280.368 58.2128 280.356 58.21 280.356C58.2815 280.219 58.1099 280.542 58.1299 280.505L58.1357 280.491L58.1471 280.465C58.1642 280.431 58.1814 280.396 58.2014 280.365C58.2071 280.356 58.21 280.353 58.21 280.35C58.1928 280.345 58.0556 280.208 57.9327 280.027C57.8069 279.845 57.6754 279.619 57.5467 279.379C57.2923 278.89 57.0521 278.341 56.8148 277.775C56.3517 276.683 55.8485 275.42 55.3054 274.505C55.0309 274.054 54.7564 273.711 54.4906 273.456C54.2247 273.213 53.9788 273.067 53.7244 272.959C53.4928 272.853 53.2727 272.839 52.9754 272.793C52.8238 272.799 52.6437 272.805 52.455 272.859C52.2635 272.899 52.0948 272.987 51.9776 273.05C51.7517 273.188 51.6202 273.313 51.483 273.479C51.3544 273.653 51.2257 273.842 51.1285 274.162C51.0342 274.485 50.9684 274.903 50.997 275.483C51.0084 276.034 51.0856 276.678 51.1399 277.286C51.1628 277.589 51.2 277.901 51.2086 278.198L51.2314 278.647C51.2314 278.721 51.2371 278.793 51.2371 278.876V278.987C51.24 279.507 51.1943 279.996 51.0913 280.399C51.037 280.596 50.977 280.779 50.8969 280.919C50.8226 281.068 50.734 281.179 50.6368 281.274L50.5625 281.345C50.5339 281.365 50.5053 281.385 50.4767 281.405C50.4481 281.431 50.4252 281.442 50.4052 281.451C50.3852 281.462 50.368 281.477 50.3538 281.477C50.2937 281.5 50.2937 281.5 50.248 281.494C50.1622 281.494 49.8363 281.328 49.539 281.008C49.2359 280.693 48.9357 280.285 48.6727 279.842C48.4068 279.399 48.1696 278.918 47.978 278.444C47.8865 278.215 47.8008 277.958 47.7636 277.809L47.6178 276.989C47.4005 275.791 47.1575 274.597 46.8688 273.405C46.5829 272.213 46.2512 271.027 45.8567 269.846C45.6623 269.257 45.445 268.669 45.2077 268.085C44.9762 267.505 44.6989 266.911 44.4501 266.385C41.9629 261.094 39.3213 255.915 36.977 250.633C36.2966 249.095 35.639 247.551 35.0329 245.999C34.4269 244.447 33.8637 242.866 33.3262 241.277C32.257 238.096 31.2964 234.875 30.433 231.625C29.5667 228.378 28.8006 225.105 28.143 221.815C27.4226 218.165 26.9566 214.472 26.6221 210.768C26.4505 208.884 26.3162 207 26.199 205.114C26.1561 204.405 26.4534 201.026 25.0611 201.358C23.6031 201.704 23.3372 207.395 23.6174 210.599C23.8175 212.903 24.1234 215.186 24.4922 217.453C25.2355 221.986 26.2304 226.46 27.3825 230.884C28.5004 235.146 29.764 239.368 31.2278 243.535C31.9539 245.616 32.7658 247.697 33.6378 249.724C34.5069 251.753 35.4189 253.754 36.348 255.74C37.5345 258.279 38.7524 260.794 39.9645 263.303L41.3968 266.288L42.103 267.78C42.2259 268.034 42.3317 268.268 42.4346 268.506C42.5433 268.74 42.6376 268.986 42.7405 269.226C43.5153 271.178 44.0842 273.262 44.5388 275.377C44.6531 275.906 44.7589 276.437 44.8618 276.969L45.0076 277.769L45.0762 278.17C45.0848 278.224 45.1163 278.387 45.1363 278.464C45.1591 278.558 45.1849 278.647 45.2106 278.73C45.4107 279.379 45.6566 279.919 45.931 280.473C46.2112 281.019 46.5257 281.551 46.9031 282.065C47.2862 282.58 47.7236 283.089 48.3497 283.538C48.6613 283.758 49.0358 283.963 49.4846 284.086C49.9306 284.212 50.4595 284.232 50.937 284.115C52.801 283.586 53.5814 281.925 53.8016 280.628C53.8673 280.285 53.8988 279.956 53.9216 279.633C53.9331 279.47 53.9331 279.313 53.9417 279.153V278.864C53.9417 278.79 53.9331 278.713 53.9274 278.638C53.9216 278.564 53.9131 278.493 53.9131 278.418C53.9131 278.384 53.9131 278.347 53.9302 278.312C53.9445 278.275 53.996 278.278 54.0246 278.298C54.1647 278.421 54.2075 278.61 54.2733 278.758L54.299 278.827L54.5077 279.31C54.6507 279.636 54.7965 279.962 54.9623 280.293C55.1281 280.625 55.3054 280.962 55.5255 281.308C55.7513 281.654 55.9972 282.014 56.4232 282.386C56.609 282.563 56.995 282.78 57.1093 282.829C57.278 282.917 57.4581 283 57.6439 283.08C58.0213 283.237 58.4101 283.389 58.8933 283.4Line truncated
fill="currentColor"
/>
<path
d="M114.542 224.625C113.558 225.051 113.498 226.697 113.281 227.569C112.2 231.936 110.228 235.961 107.912 239.794C106.137 242.712 104.107 245.473 101.951 248.2C101.414 248.886 100.871 249.569 100.322 250.292C100.047 250.664 99.7756 251.021 99.4897 251.493C99.4497 251.559 99.4268 251.599 99.3725 251.699C99.3039 251.822 99.2581 251.93 99.2067 252.053C99.1209 252.25 99.0694 252.462 99.0552 252.679V252.699C99.038 252.705 99.0494 252.711 99.0466 252.719V252.759L99.0409 252.916L99.0552 253.205L99.0752 253.542C99.1867 255.335 99.3896 257.112 99.6555 258.876C100.182 262.397 100.916 265.879 101.717 269.26C101.894 270.023 102.068 270.784 102.22 271.53C102.371 272.273 102.503 273.016 102.549 273.679C102.56 273.845 102.563 273.999 102.563 274.142L102.554 274.348C102.554 274.348 102.554 274.357 102.54 274.365C102.537 274.365 102.546 274.365 102.532 274.365L102.474 274.354C102.449 274.354 102.357 274.322 102.297 274.305C102.231 274.288 102.154 274.257 102.083 274.237C101.934 274.182 101.783 274.125 101.625 274.054C101.471 273.991 101.308 273.911 101.151 273.834L100.911 273.714L100.879 273.699L100.865 273.691C100.882 273.654 100.891 273.636 100.865 273.691C100.865 273.691 100.862 273.691 100.859 273.685C100.793 273.639 100.602 273.428 100.442 273.168C100.104 272.633 99.7899 271.944 99.4811 271.244L99.0809 270.306C98.9294 269.952 98.775 269.595 98.6177 269.252C98.3033 268.566 97.9688 267.925 97.6286 267.465C97.2883 266.994 96.9624 266.716 96.6537 266.533C96.4964 266.439 96.3449 266.371 96.182 266.316C96.019 266.265 95.856 266.253 95.6416 266.213C95.5158 266.216 95.3615 266.213 95.2099 266.259C95.047 266.265 94.8897 266.348 94.7697 266.405C94.5381 266.528 94.4209 266.648 94.3008 266.779C94.0778 267.045 93.8748 267.377 93.7691 268.051C93.6661 268.74 93.8119 269.855 93.9006 270.844C93.9234 271.095 93.9463 271.347 93.9577 271.59L93.9778 271.959L93.9835 272.141V272.282C93.9892 272.504 93.9835 272.725 93.9663 272.933C93.9377 273.351 93.872 273.742 93.7576 274.051C93.6404 274.362 93.4946 274.585 93.3088 274.751C93.263 274.794 93.203 274.825 93.1573 274.868C93.1144 274.885 93.0743 274.923 93.0458 274.925C92.9829 274.951 92.9543 274.96 92.9057 274.954C92.817 274.954 92.574 274.86 92.311 274.622C92.048 274.397 91.7821 274.074 91.5391 273.725C91.2561 273.311 90.9988 272.853 90.7787 272.379C90.6672 272.141 90.5671 271.901 90.4813 271.667C90.4384 271.55 90.3984 271.435 90.3698 271.333C90.3527 271.278 90.3412 271.233 90.3327 271.193C90.3212 271.144 90.3241 271.164 90.3069 271.064C90.1125 269.883 89.8924 268.703 89.6322 267.525C89.3721 266.348 89.0805 265.176 88.726 264.007C88.3743 262.84 87.9655 261.677 87.4566 260.534C85.9871 257.238 84.5834 253.911 83.254 250.555C82.7709 249.335 79.0886 237.427 77.0531 239.219C76.7901 239.451 76.6643 239.811 76.6443 240.165C76.6128 240.723 76.7901 241.257 76.9473 241.783C77.0817 242.226 77.2475 242.666 77.4133 243.101C79.6604 248.989 81.8818 254.911 84.5148 260.722C84.8464 261.446 85.1323 262.063 85.3925 262.738C85.6555 263.406 85.8871 264.095 86.1044 264.793C86.5361 266.188 86.882 267.625 87.185 269.077C87.3337 269.803 87.4709 270.535 87.5967 271.27L87.6424 271.544L87.6825 271.756L87.7311 271.953C87.7968 272.199 87.8712 272.407 87.9483 272.616C88.1027 273.028 88.2743 273.411 88.4629 273.791C88.8432 274.548 89.2835 275.28 89.881 275.983C90.184 276.335 90.5242 276.68 90.9731 276.992C91.419 277.295 91.988 277.589 92.6998 277.644C93.1458 277.687 93.6204 277.604 94.0149 277.447C94.2236 277.372 94.3923 277.272 94.5695 277.172C94.7268 277.069 94.8726 276.963 95.0155 276.849C95.5873 276.377 95.999 275.746 96.242 275.131C96.4907 274.514 96.6022 273.911 96.6565 273.339C96.6823 273.053 96.6966 272.776 96.6994 272.499C96.6994 272.113 96.6994 272.247 96.6937 272.199L96.6651 272.016C96.6594 271.981 96.6537 271.944 96.6537 271.904C96.648 271.861 96.6823 271.824 96.7137 271.841C96.788 271.878 96.8252 271.967 96.8767 272.061L96.9138 272.139L96.9253 272.159L96.971 272.264L97.1483 272.665C97.3913 273.202 97.6486 273.751 97.9745 274.314C98.1432 274.597 98.3261 274.885 98.5634 275.183C98.8036 275.474 99.1009 275.8 99.6012 276.075C100.144 276.355 100.708 276.62 101.359 276.838C101.437 276.866 101.528 276.889 101.614 276.915C101.7 276.938 101.783 276.963 101.897 276.986L102.057 277.021C102.106 277.032 102.166 277.041 102.234 277.049C102.366 277.061 102.497 277.086 102.629 277.086C103.155 277.115 103.672 276.992 104.107 276.726C104.536 276.446 104.859 276.023 105.033 275.534C105.079 275.411 105.119 275.288 105.145 275.157C105.176 275.028 105.205 274.9 105.219 274.774L105.242 274.582L105.253 274.437C105.259 274.345 105.265 274.251 105.265 274.165C105.273 273.339 105.165 272.633 105.053 271.933C104.936 271.238 104.796 270.564 104.65 269.901L103.755 265.967C103.163 263.369 102.64 260.78 102.277 258.176C102.151 257.29 102.048 256.401 101.968 255.512C101.928 255.069 101.894 254.623 101.863 254.18L101.823 253.514L101.808 253.182L101.803 253.028V253.016C101.803 253.016 101.805 253.005 101.808 252.999L101.814 252.991C101Line truncated
fill="currentColor"
/>
<path
d="M93.7519 80.3367C93.7262 80.511 93.8005 80.9398 94.1865 81.3685C94.5638 81.7973 95.2328 82.1574 96.039 82.2489C96.8424 82.3375 97.7887 82.1974 98.652 81.6258C99.0809 81.3428 99.4811 80.9398 99.7556 80.391C99.8871 80.1137 99.9786 79.7993 100.01 79.4677C100.036 79.1333 99.99 78.7903 99.8814 78.4645C99.6584 77.8099 99.1752 77.3097 98.6749 77.001C98.1689 76.6952 97.6314 76.5294 97.1168 76.4751C96.0819 76.3636 95.1442 76.6094 94.4037 77.0296C93.6604 77.444 93.1458 78.0729 92.8914 78.6159C92.6312 79.1647 92.6284 79.5821 92.6198 79.6049C92.6026 79.8765 92.7827 80.0765 93.0458 80.1509C93.3059 80.2309 93.6318 80.2023 93.9492 80.1337C93.852 80.1623 93.7662 80.2309 93.7519 80.3309V80.3367Z"
fill="currentColor"
/>
<path
d="M128.613 80.5881C129.291 80.8053 130.08 80.228 130.377 79.3019C130.675 78.3758 130.369 77.4468 129.691 77.2296C129.014 77.0124 128.224 77.5897 127.927 78.5158C127.63 79.4419 127.936 80.3709 128.613 80.5881Z"
fill="currentColor"
/>
<path
d="M131.778 75.3888C132.441 75.526 133.15 74.8257 133.356 73.8253C133.562 72.8249 133.193 71.9017 132.527 71.7645C131.863 71.6273 131.154 72.3276 130.949 73.328C130.743 74.3284 131.112 75.2516 131.778 75.3888Z"
fill="currentColor"
/>
<path
d="M123.684 157.214L123.084 154.867C122.875 154.087 122.704 153.295 122.441 152.538L120.986 147.964C120.025 144.929 119.073 141.893 118.149 138.846C116.008 131.755 113.89 124.649 112.123 117.5C111.688 115.714 111.277 113.925 110.934 112.135C110.848 111.689 110.762 111.241 110.691 110.795C110.611 110.349 110.542 109.903 110.479 109.46C110.253 107.874 109.767 106.119 110.382 104.561C110.888 103.277 112.02 102.431 112.966 101.488C114.593 99.8675 116.011 98.0553 117.483 96.2974C118.939 94.5624 120.548 92.9675 121.958 91.1896C122.303 90.7523 122.506 90.3121 122.712 89.8005C122.721 89.7118 122.724 89.6175 122.712 89.5146C122.675 89.0973 122.481 88.76 122.218 88.4628C122.098 88.3341 121.96 88.2141 121.817 88.1026C122.083 87.9968 122.349 87.8911 122.612 87.7882L124.742 86.9364C124.948 86.8564 125.099 86.622 125.342 86.682C125.477 86.7135 125.565 86.6734 125.631 86.5848C126.174 86.7964 126.738 86.9021 127.315 86.9021C128.55 86.9021 129.848 86.4219 131.115 85.4729C132.761 84.2382 134.394 82.2116 135.832 79.6134C137.464 76.6665 138.296 74.537 138.533 72.7048C138.959 69.4149 137.27 67.7428 135.777 66.9167C134.885 66.4222 133.696 66.0364 132.341 66.2507C132.175 66.2164 131.998 66.2193 131.829 66.2222C131.263 66.2336 130.694 66.2965 130.125 66.2765C128.587 66.2164 127.055 66.0649 125.525 65.9592C124.03 65.8563 122.541 65.7362 121.088 65.5362C120.365 65.4361 119.65 65.3132 118.99 65.1474C118.658 65.0674 118.344 64.9674 118.087 64.8644C118.029 64.8387 117.949 64.813 117.909 64.7873C117.909 64.7873 117.878 64.7701 117.869 64.7673L117.526 64.4643C116.371 63.4467 115.253 62.3749 114.03 61.4087C113.99 61.3773 113.947 61.3459 113.907 61.3144C114.073 61.1744 114.227 61.0229 114.379 60.8714C115.059 60.1911 115.682 59.4651 116.357 58.7905C117.272 57.8701 118.192 57.0069 119.104 55.9608L119.447 55.5835L119.762 55.1862L120.388 54.3916C120.788 53.8542 121.16 53.2997 121.534 52.748C122.938 50.5585 124.099 48.2919 125.28 46.0996C126.463 43.913 127.65 41.7635 129.125 39.9056C129.505 39.4568 129.86 38.9766 130.283 38.5765L130.894 37.9534C131.106 37.7561 131.332 37.5789 131.552 37.3903L131.881 37.1101C131.989 37.0158 132.098 36.9215 132.221 36.8472L132.924 36.3584L133.273 36.1126C133.405 36.0297 133.553 35.9354 133.71 35.8268L133.831 35.741L133.896 35.6924L133.988 35.621C134.108 35.5266 134.228 35.4237 134.339 35.3151C134.565 35.1007 134.757 34.8578 134.903 34.5948C135.068 34.309 135.16 33.9946 135.217 33.6773C135.234 33.5401 135.226 33.4 135.237 33.26C135.237 33.1228 135.217 32.9827 135.197 32.8455C135.163 32.7112 135.137 32.574 135.08 32.4454L135.003 32.251L134.903 32.0652C134.637 31.5707 134.185 31.1791 133.676 30.8904C133.547 30.819 133.419 30.7504 133.279 30.6961C133.17 30.6503 133.07 30.6132 132.967 30.5789C132.758 30.5074 132.55 30.456 132.338 30.4188C132.127 30.3788 131.906 30.3588 131.681 30.3502L131.137 30.3616C129.682 30.4217 128.324 30.9047 127.164 31.5136C124.831 32.7569 123.075 34.4205 121.214 35.6638C118.919 37.2359 116.426 38.5479 113.79 39.4711C112.252 40.0171 110.699 40.4115 109.061 40.7545C107.423 41.1061 105.713 41.4376 103.995 42.0093C102.277 42.5924 100.602 43.4299 99.135 44.5132C97.6627 45.5994 96.3447 46.8885 95.2469 48.3491C94.5894 49.2209 94.1605 50.2556 93.5544 51.1188C93.0427 51.8877 92.7968 52.6623 92.774 53.3997C92.7711 53.7713 92.7911 54.1229 92.8855 54.4716C92.8912 54.4945 92.8997 54.5145 92.9055 54.5345C92.8569 54.5431 92.8083 54.5516 92.7597 54.5631C92.2251 54.7031 91.679 54.8117 91.1587 54.9947L90.3725 55.2519L89.6035 55.5549L89.2204 55.7064L88.8458 55.8779L88.1025 56.2266C87.8081 56.3667 87.5479 56.5696 87.2706 56.744C86.9961 56.9183 86.7188 57.0784 86.4215 57.1899C86.3243 57.2213 86.2357 57.2613 86.1528 57.3013C85.7783 55.6406 85.2122 54.14 84.7862 52.6966C84.5689 51.9677 84.3831 51.2531 84.263 50.55C84.143 49.8468 84.0829 49.1237 84.0629 48.3748C84.0172 46.877 84.1201 45.3021 84.1201 43.6471C84.1201 42.8211 84.0886 41.9722 83.9886 41.1118C83.8914 40.2486 83.7141 39.3882 83.4768 38.5593C83.0108 36.8958 82.3076 35.3351 81.5071 33.8745C81.1011 33.1457 80.6751 32.4339 80.2263 31.7479C80.0004 31.4078 79.7746 31.0648 79.5401 30.7332L79.3657 30.4817L79.2114 30.2645L79.037 30.0272L78.3366 29.0725C78.2994 29.0097 78.2565 28.9925 78.2165 28.9611L78.0935 28.8753L77.8477 28.7038L77.3588 28.3637C77.0672 28.2379 76.6412 28.1922 76.2324 28.2779C75.8236 28.358 75.4634 28.5752 75.2432 28.7753C75.0059 28.9982 74.7372 29.3327 74.6028 29.8472L73.8881 32.4339L73.2963 34.2261L72.999 35.1208L72.6617 36.0068L71.9841 37.779C71.0092 40.1228 69.9829 42.4523 68.8622 44.7676C67.2926 48.069 65.6688 51.376 64.1279 54.7546C62.341 58.6848 60.6943 62.7522 59.6851 67.0539L59.3535 68.6775C59.2449 69.2205 59.1191 69.7579 59.0648 70.3124L58.8503 71.9617C58.7731 72.5105 58.7188 73.0621 58.6931 73.6452C58.6702 74.2255 58.6674 74.8286 58.7331 75.4488C58.796 76.0348 58.9018 76.6436 59.1219 77.2353C59.0162 77.3096 58.9075 77.3982 58.8017 77.5011C58.473 77.8126 58.1899 78.2271 57.9241 78.6187C57.2694 79.582 56.6661 80.5766 55.9914 81Line truncated
fill="currentColor"
/>
<path
d="M25.4785 195.044C25.9988 194.658 25.5871 193.975 25.3527 193.535C25.0611 192.994 24.6865 192.503 24.2977 192.031C23.6859 191.288 23.0484 190.562 22.3251 189.922C21.719 189.387 21.0843 188.896 20.4611 188.39C19.7464 187.812 19.0259 187.226 18.2483 186.7C18.0053 186.532 17.748 186.38 17.4936 186.229C16.6845 185.766 15.8268 185.354 14.9091 185.025C13.0051 184.342 10.7437 184.165 8.85685 185.005C8.03635 185.371 7.34735 185.943 6.83847 186.643C6.11803 187.632 5.76353 188.876 5.92077 190.145C6.05228 191.191 6.45538 192.228 7.17868 193.026C7.77333 193.706 8.57382 194.209 9.44292 194.489C10.3177 194.749 11.2612 194.801 12.156 194.564C13.0794 194.335 13.8628 193.76 14.5032 193.109C14.8234 192.774 15.1007 192.403 15.3322 192C15.5495 191.591 15.7353 191.159 15.8354 190.702C16.0184 189.962 16.0184 189.176 15.8468 188.427C16.3043 188.664 16.7274 188.978 17.1562 189.276C17.8738 189.785 18.4198 190.228 19.0945 190.805C18.9888 191.122 18.4084 192.574 18.4084 192.574C18.3655 192.726 18.2798 192.863 18.2197 193.006C18.1511 193.149 18.0911 193.295 18.0196 193.435C17.7051 193.981 17.3678 194.518 16.9361 194.981C16.1013 195.927 15.0406 196.687 13.8571 197.133C12.9022 197.508 11.873 197.605 10.8695 197.605C10.0747 197.608 9.28283 197.479 8.51378 197.282C7.75046 197.059 7.02144 196.719 6.34674 196.29C4.39126 194.998 2.99326 192.811 2.77027 190.448L2.74168 189.939C2.73882 189.853 2.73024 189.77 2.72738 189.684L2.73882 189.43L2.76169 188.921C2.77313 188.753 2.796 188.584 2.82173 188.415C2.92465 187.747 3.11619 187.089 3.38493 186.472C3.91668 185.231 4.77149 184.148 5.835 183.308C6.49826 182.782 7.25015 182.344 8.01634 182.056C9.22279 181.607 10.6322 181.461 12.0245 181.535C12.7249 181.575 13.4139 181.661 14.1086 181.821C14.8119 181.964 15.5038 182.173 16.1928 182.399C19.0488 183.376 21.7219 184.994 24.3635 186.755C24.5607 186.883 24.758 187.018 24.9696 187.121C26.1789 187.701 26.9851 186.46 26.379 185.291C25.933 184.428 25.2955 183.925 24.4864 183.453C23.4 182.816 22.3537 182.11 21.233 181.53C19.7264 180.752 18.1625 180.083 16.5387 179.595C16.3843 179.549 16.2271 179.509 16.0698 179.466C15.6124 178.98 15.0778 178.111 14.8033 177.714C14.0343 176.611 13.3139 175.439 12.8136 174.161C12.3161 172.883 11.9873 171.528 11.8959 170.196C11.833 169.245 11.8787 168.081 12.4305 167.261C12.6678 166.909 13.0022 166.629 13.2195 166.263C13.5111 165.772 13.5597 165.137 13.3339 164.608C13.1709 164.223 12.8936 163.902 12.5706 163.674C12.4105 163.557 12.2389 163.462 12.0531 163.388C9.38575 162.693 6.74699 164.254 5.63774 166.718C5.37186 167.31 5.14315 167.904 4.96304 168.499C4.57995 169.685 4.37982 170.885 4.2769 172.092C4.17684 173.295 4.21115 174.507 4.38268 175.716C4.55708 176.951 4.9659 178.163 5.54053 179.34C5.65203 179.557 5.75495 179.78 5.85501 180.009C5.86073 180.018 5.86645 180.029 5.86931 180.038C5.72922 180.109 5.59199 180.186 5.45477 180.263C4.43414 180.852 3.50786 181.598 2.72452 182.479C1.39514 183.971 0.488871 185.851 0.157239 187.827C0.114356 188.072 0.080049 188.324 0.054319 188.573C0.0343067 188.821 0.0285889 189.073 0.0142945 189.324L0 189.702C0 189.827 0.0114356 189.953 0.0171534 190.079L0.0628956 190.831C0.197264 191.828 0.420257 192.817 0.811926 193.746C1.60384 195.656 3.00184 197.262 4.67143 198.431C5.51195 199.008 6.41821 199.509 7.3931 199.826L7.75618 199.954L8.12783 200.052C8.37656 200.112 8.62242 200.18 8.874 200.232C9.38003 200.3 9.88319 200.406 10.3978 200.409C12.4419 200.535 14.5289 200.026 16.3071 199.003C17.2248 198.488 18.0253 197.791 18.7343 197.028L19.2432 196.433C19.4004 196.224 19.5463 196.007 19.6978 195.796C19.855 195.587 19.9808 195.358 20.1095 195.132C20.2352 194.904 20.381 194.687 20.4811 194.446C20.6326 194.095 20.8385 193.766 20.9728 193.403C21.0643 193.18 21.1615 192.954 21.2444 192.726C21.4531 192.963 21.6561 193.203 21.862 193.443C22.3451 194.009 22.7168 194.718 23.38 195.101C24.0204 195.467 24.8752 195.49 25.4813 195.041L25.4785 195.044ZM10.8181 191.985C10.0319 191.965 9.32857 191.485 8.95977 190.842C8.83398 190.642 8.75393 190.422 8.69103 190.202C8.59955 189.856 8.57954 189.482 8.66245 189.127C8.74249 188.776 8.89973 188.444 9.1313 188.167C9.7717 187.389 11.0353 187.166 11.9502 187.532C12.5791 187.835 13.0709 188.461 13.1852 189.159C13.4282 190.551 12.2018 192.023 10.8181 191.985ZM7.08148 172.512C7.13866 171.457 7.25015 170.397 7.53032 169.405C7.73616 168.682 8.01634 167.99 8.38513 167.41L8.52522 167.192C8.57382 167.121 8.62814 167.064 8.67674 166.995C8.77394 166.858 8.8883 166.746 8.99694 166.635C9.05126 166.575 9.10843 166.524 9.16847 166.481L9.34286 166.338C9.4029 166.295 9.4658 166.263 9.52869 166.223C9.58301 166.189 9.58873 166.181 9.60302 166.178C9.72309 166.146 9.96324 166.103 9.87747 166.289C9.82601 166.403 9.72595 166.501 9.67164 166.618C9.61732 166.738 9.57443 166.861 9.53727 166.987C9.20564 168.073 9.11129 169.225 9.17705 170.359C9.27711 172.106 9.7088 173.841 10.3435 175.47C10.7952 176.631 11.3698 177.751 12.0731 178.8C10.981 178.754 9.87748 178.817 8.77108 179.034C8.67388 179.0Line truncated
fill="currentColor"
/>
<path
d="M47.4347 136.514C47.6319 136.057 47.8721 135.617 48.1465 135.199C48.7126 134.37 49.4216 133.624 50.2535 132.981C51.2256 132.229 54.5247 130.12 53.9987 128.962C53.2754 127.373 50.7024 129.228 49.8018 129.808C48.7784 130.469 47.8378 131.238 47.0087 132.129C45.7994 133.427 44.8645 135.062 44.4214 136.811C43.984 138.495 43.9697 140.23 44.2041 141.865C44.4357 143.506 44.916 145.06 45.5049 146.544C45.8852 147.49 46.3168 148.405 46.7886 149.291C48.2923 152.101 50.1335 154.653 52.0689 157.097C53.7614 159.229 55.5282 161.287 57.3893 163.259C58.6644 164.603 59.928 165.932 61.326 167.187C62.2009 167.976 63.1243 168.653 63.9133 169.425C64.8968 170.379 65.7459 171.374 66.3777 172.492C66.8208 173.266 67.1868 174.115 67.5241 174.999C65.6401 175.422 63.7961 176.085 62.0779 176.911C61.2145 177.325 60.3769 177.771 59.5621 178.231C58.8731 178.617 58.1927 179.02 57.5066 179.415C56.666 179.898 55.6368 180.404 55.4539 181.481C55.3252 182.244 55.857 182.982 56.5031 183.413C57.5837 184.137 58.8016 184.597 60.0452 184.957C60.6771 185.14 61.3175 185.297 61.9521 185.46C62.5268 185.609 63.3158 185.694 63.8161 186.009C63.2701 186.612 62.5239 186.975 61.812 187.341C61.0859 187.715 60.4083 188.158 59.6993 188.564C59.2734 188.807 58.8531 189.09 58.5672 189.49C58.2813 189.89 58.1469 190.428 58.3385 190.882C58.4614 191.174 58.6987 191.4 58.9474 191.594C60.8029 193.032 63.2672 193.063 65.4771 192.632C67.1382 192.309 68.7734 191.765 70.22 190.874C71.7753 189.913 73.1304 188.558 74.4055 187.264C75.9207 185.712 77.3301 184.074 78.6309 182.333C79.2799 181.461 79.8974 180.558 80.4663 179.609C81.2983 178.226 82.0759 176.691 82.3589 175.087C82.5848 173.804 82.2274 172.746 81.8815 171.534C81.4784 170.137 81.0467 168.762 80.6064 167.39C80.0404 165.646 79.3799 163.943 78.8196 162.208C78.1278 160.064 77.4616 157.92 76.8727 155.768C76.2895 153.615 75.7749 151.452 75.4604 149.296C75.4032 148.836 75.2488 148.459 75.0573 148.142C74.8686 147.824 74.617 147.561 74.3226 147.33C73.5535 146.721 72.8016 147.101 72.6558 148.293C72.5986 148.722 72.6844 149.176 72.7445 149.611C72.916 150.677 73.1904 151.709 73.4106 152.744C73.9995 155.551 74.7285 158.294 75.5605 160.993C76.381 163.634 77.2701 166.238 78.1249 168.833C78.6709 170.494 79.5257 172.112 79.6515 173.881C79.7888 175.819 78.5623 177.645 77.5217 179.186C76.7926 180.272 75.9807 181.324 75.1459 182.353C73.9452 183.822 72.6701 185.237 71.2807 186.532C69.9056 187.815 68.5362 188.956 66.7122 189.516C65.8259 189.787 63.4016 190.848 62.6125 190.03C63.3444 189.39 64.2907 188.984 65.0969 188.441L65.6601 188.061C65.8945 187.898 66.1118 187.727 66.3348 187.558L66.6693 187.304L66.9752 187.043C67.6613 186.44 68.9679 185.666 69.3681 184.831C69.6483 184.245 69.3853 184.299 68.7163 184.125C67.6242 183.839 66.4663 183.765 65.3571 183.531L64.6452 183.368L64.1563 183.25C63.1929 183.013 62.238 182.75 61.306 182.459C60.84 182.316 60.3826 182.156 59.9338 181.993L59.6021 181.864L59.4363 181.801C59.4363 181.801 59.3963 181.787 59.3877 181.781L59.3734 181.776C59.342 181.761 59.2848 181.736 59.2733 181.727C59.2505 181.71 59.219 181.701 59.2105 181.673C59.2105 181.638 59.2562 181.636 59.2762 181.621L59.3677 181.573L59.948 181.244C60.4112 180.984 60.8686 180.724 61.3289 180.478C63.0271 179.56 64.8453 178.912 66.6722 178.294C68.0616 177.825 71.4265 177.554 70.7061 175.447C69.8627 172.978 68.6019 170.457 66.8408 168.513C65.5801 167.121 64.1935 165.981 62.9242 164.846C61.6691 163.717 60.4255 162.508 59.2362 161.279C58.0469 160.044 56.9605 158.715 55.8513 157.417C53.6899 154.879 51.6515 152.232 49.9791 149.402C48.9956 147.73 48.1037 146.001 47.5262 144.2C46.8629 142.111 46.5627 139.93 46.9944 137.949C47.0973 137.454 47.2574 136.98 47.4461 136.517L47.4347 136.514Z"
fill="currentColor"
/>
<path
d="M160.078 19.6973C160.047 19.7087 160.027 19.7173 160.012 19.7231C160.029 19.7173 160.055 19.7087 160.078 19.6973Z"
fill="currentColor"
/>
<path
d="M160.012 19.723C160.012 19.723 159.995 19.7288 159.989 19.7316C159.992 19.7316 160.001 19.7259 160.012 19.723Z"
fill="currentColor"
/>
<path
d="M150.327 36.6185C150.327 36.6185 150.327 36.63 150.321 36.6385C150.321 36.63 150.324 36.6242 150.327 36.6185Z"
fill="currentColor"
/>
<path
d="M150.309 36.6901C150.269 36.8622 150.289 36.7761 150.309 36.6901V36.6901Z"
fill="currentColor"
/>
<path
d="M187.781 56.3986C187.781 56.3986 187.793 56.3925 187.799 56.3895C187.793 56.3895 187.787 56.3955 187.781 56.3986Z"
fill="currentColor"
/>
<path
d="M190.651 38.648C190.608 38.5966 190.559 38.5451 190.511 38.4994C190.451 38.4451 190.385 38.3937 190.316 38.3479C190.188 38.2565 190.005 38.1736 189.83 38.1278L189.782 38.1164H189.767L189.736 38.1078L189.493 38.0564L188.515 37.8563C187.743 37.7019 186.963 37.5933 186.194 37.4333C186.194 37.4333 180.344 36.4043 180.121 36.2613C180.033 34.9637 179.801 33.6831 179.41 32.4312C179.275 32.0053 179.118 31.5851 178.946 31.1707C179.032 31.0535 182.689 27.5149 183.658 26.5459C184.144 26.0628 184.693 25.637 185.173 25.1425C185.408 24.8967 185.642 24.6508 185.876 24.3907C185.994 24.265 186.111 24.1335 186.228 23.9934L186.317 23.8905L186.36 23.8391L186.382 23.8133L186.44 23.7419C186.471 23.699 186.503 23.6533 186.531 23.6075C186.651 23.416 186.711 23.2331 186.748 23.033C186.763 22.9416 186.768 22.8701 186.771 22.7844C186.771 22.7015 186.768 22.6186 186.76 22.5643C186.746 22.4385 186.714 22.3127 186.686 22.227C186.671 22.1812 186.651 22.1298 186.634 22.0898L186.58 21.9754C186.428 21.6868 186.265 21.501 186.039 21.3123C185.985 21.2666 185.914 21.2208 185.848 21.178C185.776 21.1351 185.691 21.0894 185.608 21.0522C185.408 20.9722 185.179 20.8979 184.83 20.9179C184.667 20.9264 184.484 20.9693 184.307 21.0436C184.187 21.0951 184.221 21.0836 184.204 21.0922L184.175 21.1065L184.064 21.1665L183.838 21.2866L182.943 21.7725C182.24 22.1584 181.562 22.5843 180.862 22.9644C180.862 22.9644 175.144 26.06 175.078 26.1029C174.432 25.6312 173.732 25.2339 173.003 24.9167C173.023 24.7795 173.071 19.9832 173.103 18.5169C173.16 15.65 173.174 12.7717 173.203 9.8876C173.22 8.44701 173.317 7.00356 173.297 5.54867C173.289 4.83981 173.269 4.12523 173.234 3.40493C173.226 3.0505 173.189 2.67606 173.166 2.31305L173.146 2.03865V1.99292L173.123 1.86144C173.109 1.77283 173.089 1.68708 173.063 1.60133C173.006 1.43555 172.931 1.25833 172.834 1.12399C172.64 0.826723 172.405 0.640932 172.168 0.498016C171.934 0.366533 171.696 0.280783 171.456 0.240767C171.216 0.197892 170.964 0.189317 170.69 0.240767C170.424 0.283642 170.101 0.406549 169.815 0.632357C169.518 0.866739 169.292 1.22403 169.195 1.56989L169.123 1.85572L168.872 2.879L168.371 4.92842C167.98 6.54908 167.631 8.18404 167.245 9.81043C166.35 13.6034 164.257 23.5904 164.163 24.3107C163.914 24.3621 163.852 24.3793 163.606 24.4536C163.123 24.5679 162.654 24.7537 162.182 24.9224C161.833 25.0767 161.482 25.2282 161.141 25.4026C161.081 25.3511 157.874 21.7211 156.979 20.7378C156.533 20.2462 156.155 19.6916 155.707 19.2029L155.046 18.4883L154.963 18.3997C154.946 18.3825 154.983 18.4168 154.877 18.3139C154.8 18.2425 154.714 18.1767 154.626 18.1196C154.537 18.0653 154.454 18.0252 154.36 17.9881C154.269 17.9509 154.168 17.9223 154.1 17.9109C154.023 17.8966 153.943 17.8852 153.865 17.8795C153.785 17.8738 153.737 17.8795 153.671 17.8795C153.54 17.8852 153.445 17.9052 153.351 17.9281C153.165 17.9738 153.031 18.0338 152.905 18.0996C152.659 18.2368 152.467 18.3854 152.264 18.6369C152.216 18.6998 152.162 18.7798 152.116 18.857C152.064 18.9513 152.019 19.0428 151.979 19.16C151.898 19.403 151.841 19.7259 151.939 20.0975C151.95 20.1433 151.964 20.1861 151.979 20.229L151.993 20.269L152.027 20.3548L152.073 20.4691L152.164 20.6978L152.35 21.1551L152.733 22.0641C153.039 22.7786 156.095 30.1017 156.19 30.1817C156.098 30.336 156.01 30.4904 155.924 30.6476C155.409 31.6108 155.006 32.617 154.694 33.6431C154.546 33.6803 149.134 34.169 147.684 34.3205C146.958 34.3977 146.224 34.3834 145.495 34.4692C145.137 34.512 144.783 34.5578 144.42 34.6092L143.874 34.6921L143.593 34.7379L143.453 34.7607H143.436L143.385 34.7722L143.302 34.7922C143.19 34.8207 143.079 34.8636 142.979 34.9122C142.79 35.0094 142.61 35.1552 142.51 35.2752C142.275 35.5468 142.207 35.7497 142.144 35.9383C142.092 36.1156 142.072 36.2728 142.067 36.4185C142.064 36.7158 142.09 36.9845 142.267 37.3589C142.373 37.5562 142.504 37.7762 142.819 37.9963C142.956 38.0964 143.156 38.185 143.345 38.2307L143.416 38.245L143.682 38.2993L144.746 38.5137C145.586 38.6766 146.429 38.791 147.261 38.9567L153.036 40.0829C153.319 40.1372 153.908 40.2316 153.92 40.2401C153.92 40.9518 153.948 41.6693 154.023 42.3839C154.106 43.1699 154.248 43.9559 154.449 44.7305C154.311 44.9106 150.586 48.7922 149.623 49.8126C149.143 50.3214 148.594 50.7702 148.116 51.2904C147.647 51.802 147.196 52.308 146.727 52.8825L146.658 52.9682C146.521 53.1455 146.421 53.3512 146.375 53.5428C146.321 53.7571 146.321 53.9 146.327 54.0715C146.332 54.1487 146.341 54.203 146.355 54.2688C146.367 54.3317 146.384 54.4003 146.398 54.4431C146.532 54.8462 146.701 55.0462 146.898 55.2492C146.998 55.3464 147.113 55.4378 147.253 55.5264C147.319 55.5693 147.413 55.615 147.496 55.6522C147.599 55.6922 147.699 55.7265 147.827 55.7551C148.085 55.8065 148.451 55.8094 148.811 55.6436L148.894 55.6036L148.994 55.5493L149.903 55.0405C150.618 54.6346 151.307 54.1887 152.019 53.7886C152.019 53.7886 157.691 50.5329 157.719 50.5272C157.859 50.6587 157.999 50.793 158.148 50.916C158.465 51.2018 159.334 51.9192 159.861 52.1593C159.712 52.Line truncated
fill="currentColor"
/>
<path
d="M165.993 34.532C165.956 34.3548 165.896 34.1833 165.81 34.0147C165.676 33.7488 165.481 33.4744 165.201 33.24C164.924 33.0057 164.555 32.8313 164.212 32.7627C164.066 32.737 163.92 32.7113 163.769 32.7084C163.692 32.7084 163.626 32.6998 163.537 32.7084C163.449 32.7141 163.36 32.7255 163.271 32.7398C162.897 32.7999 162.451 33.0457 162.191 33.3744C162.005 33.5945 161.891 33.8146 161.813 34.0118C161.733 34.2147 161.693 34.3948 161.679 34.572C161.673 34.6321 161.679 34.6921 161.688 34.7521C161.699 34.8236 161.708 34.855 161.731 34.9779L161.725 34.8493C161.748 34.9436 161.773 35.0379 161.785 35.1323C161.816 35.3838 161.873 35.6439 161.985 35.9126C162.096 36.1784 162.265 36.4642 162.534 36.7186C162.654 36.8301 162.977 36.5986 163.303 36.1555C163.392 36.0383 163.469 35.9154 163.546 35.7925C163.672 36.0526 163.772 36.2613 163.852 36.4299C163.96 36.6472 164.043 36.8072 164.115 36.9216C164.255 37.1559 164.338 37.2245 164.452 37.2789C164.509 37.2989 164.547 37.2846 164.587 37.2703C164.855 37.1931 165.084 37.0502 165.284 36.8758C165.487 36.7043 165.661 36.4757 165.79 36.2184C165.913 35.964 165.99 35.7182 166.03 35.4124C166.039 35.3409 166.045 35.2695 166.047 35.2009C166.047 35.1294 166.053 35.0808 166.047 34.9865C166.042 34.8064 166.019 34.6692 165.99 34.5263L165.993 34.532ZM163.729 34.7921C163.666 34.795 163.689 34.7921 163.729 34.7921V34.7921ZM164.515 34.7378V34.7064V34.7235V34.7321V34.735V34.7378Z"
fill="currentColor"
/>
<path
d="M172.188 41.4463C172.151 41.2691 172.091 41.0976 172.005 40.929C171.871 40.6631 171.676 40.3887 171.396 40.1544C171.119 39.92 170.75 39.7456 170.407 39.677C170.261 39.6513 170.116 39.6256 169.964 39.6227C169.887 39.6227 169.821 39.6141 169.732 39.6227C169.644 39.6284 169.555 39.6399 169.467 39.6541C169.092 39.7142 168.646 39.96 168.386 40.2887C168.2 40.5088 168.086 40.7289 168.008 40.9261C167.928 41.129 167.888 41.3091 167.874 41.4863C167.868 41.5464 167.874 41.6064 167.883 41.6664C167.894 41.7379 167.903 41.7693 167.926 41.8922L167.92 41.7636C167.943 41.8579 167.968 41.9522 167.98 42.0466C168.011 42.2981 168.069 42.5582 168.18 42.8269C168.292 43.0956 168.46 43.3785 168.729 43.6329C168.849 43.7444 169.172 43.5129 169.498 43.0698C169.587 42.9527 169.664 42.8297 169.741 42.7068C169.867 42.9669 169.967 43.1756 170.047 43.3442C170.156 43.5615 170.238 43.7215 170.31 43.8359C170.45 44.0703 170.533 44.1389 170.647 44.1932C170.704 44.2132 170.742 44.1989 170.782 44.1846C171.05 44.1074 171.279 43.9645 171.479 43.7901C171.682 43.6186 171.857 43.39 171.985 43.1327C172.108 42.8783 172.185 42.6325 172.225 42.3267C172.234 42.2552 172.24 42.1838 172.243 42.1152C172.243 42.0437 172.248 41.9951 172.243 41.9008C172.237 41.7207 172.214 41.5835 172.185 41.4406L172.188 41.4463ZM170.71 41.655V41.6235V41.6407V41.6493V41.6521V41.655Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_1_2360">
<rect
width="196"
height="284"
fill="white"
transform="translate(0 0.200684)"
/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 59 KiB

@@ -0,0 +1,3 @@
{% load wagtailadmin_tags %}
{% help_block status="info" %}Navn og e-post må endres i Galtinn.{% endhelp_block %}
{% include "wagtailadmin/account/settings_panels/base.html" %}
@@ -0,0 +1,103 @@
{% extends "wagtailadmin/admin_base.html" %}
{% load i18n static wagtailadmin_tags %}
{% block titletag %}{% trans "Sign in" %}{% endblock %}
{% block bodyclass %}login{% endblock %}
{% block furniture %}
<style>
.galtinn-login { margin-bottom: 2em; }
.local-login-toggle {
background: none;
border: 0;
padding: 0;
color: inherit;
cursor: pointer;
font-size: 0.85em;
text-decoration: underline;
}
.guard-pig {
height: 8rem;
background-color: currentColor;
-webkit-mask: url("{% static 'graphics/guard-pig.svg' %}") no-repeat center / contain;
mask: url("{% static 'graphics/guard-pig.svg' %}") no-repeat center / contain;
}
</style>
<main class="content-wrapper" id="main">
<h1>cms.neuf.no</h1>
<div class="messages" role="status">
{# Always show messages div so it can be appended to by JS #}
{% if messages or form.errors %}
<ul>
{% for error in form.non_field_errors %}
<li class="error">{{ error }}</li>
{% endfor %}
{% for message in messages %}
<li class="{{ message.tags }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if oidc_enabled %}
<div class="galtinn-login">
<a class="button" href="{% url 'oidc_login' %}{% if next %}?next={{ next|urlencode }}{% endif %}">
Logg inn med Galtinn
</a>
</div>
<button type="button" class="local-login-toggle" aria-expanded="false"
aria-controls="local-login" {% if form.errors %}hidden{% endif %}>
Logg inn med lokal bruker
</button>
{% endif %}
<div class="local-login" id="local-login" {% if oidc_enabled and not form.errors %}hidden{% endif %}>
<form class="login-form" action="{% url 'wagtailadmin_login' %}" method="post" autocomplete="off" novalidate>
{% csrf_token %}
{% url 'wagtailadmin_home' as home_url %}
<input type="hidden" name="next" value="{{ next|default:home_url }}" />
{% formattedfield form.username %}
{% formattedfield form.password %}
{% if show_password_reset %}
<a class="reset-password" href="{% url 'wagtailadmin_password_reset' %}">{% trans "Forgotten password?" %}</a>
{% endif %}
{% for field_name, field in form.extra_fields %}
{% formattedfield field %}
{% endfor %}
{% include "wagtailadmin/shared/forms/single_checkbox.html" with label_classname="remember-me" name="remember" text=_("Remember me") %}
<footer class="form-actions">
<button
type="submit"
class="button button-longrunning"
data-controller="w-progress"
data-action="w-progress#activate"
data-w-progress-active-value="{% trans 'Signing in…' %}"
>
{% icon name="spinner" %}
<em data-w-progress-target="label">{% trans 'Sign in' %}</em>
</button>
</footer>
</form>
</div>
<div class="login-logo">
<div class="guard-pig" aria-hidden="true"></div>
</div>
</main>
<script>
const toggle = document.querySelector(".local-login-toggle");
toggle?.addEventListener("click", () => {
document.querySelector(".local-login").hidden = false;
toggle.setAttribute("aria-expanded", "true");
toggle.hidden = true;
document.querySelector("#id_username").focus();
});
</script>
{% endblock %}
+11
View File
@@ -4,13 +4,24 @@ from django.urls import include, path
from grapple import urls as grapple_urls
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.admin.auth import require_admin_access
from wagtail.documents import urls as wagtaildocs_urls
from dnscms import oidc
urlpatterns = [
path("api/", include(grapple_urls)),
path("django-admin/", admin.site.urls),
# overrides Wagtail's account view, same auth guard as the wagtailadmin urls
path(
"admin/account/",
require_admin_access(oidc.GaltinnAccountView.as_view()),
name="wagtailadmin_account",
),
path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
path("oidc/login/", oidc.oidc_login, name="oidc_login"),
path("oidc/callback/", oidc.oidc_callback, name="oidc_callback"),
]
+2
View File
@@ -13,6 +13,8 @@ dependencies = [
"psycopg2-binary>=2.9.12,<3",
"gunicorn>=26.2.0,<27",
"whitenoise>=6.12.0,<7",
"authlib>=1.6,<2",
"requests>=2.34,<3",
]
[dependency-groups]
+377
View File
@@ -0,0 +1,377 @@
import logging
import re
import pytest
import requests
from authlib.integrations.django_client import OAuthError
from django.contrib.auth.models import Group, Permission, User
from django.http import HttpResponseRedirect
from django.urls import reverse
from joserfc.errors import JoseError
from dnscms import oidc
from dnscms.models import GaltinnUser
from dnscms.oidc import WAGTAIL_EDITORS_GROUP, LoginRefused, resolve_user
pytestmark = pytest.mark.django_db
EDITOR_GROUP = "Aktive"
def claims(**overrides):
base = {
"sub": "42",
"preferred_username": "kari",
"email": "kari@neuf.test",
"given_name": "Kari",
"family_name": "Nordmann",
"groups": [EDITOR_GROUP, "Styret"],
}
return {**base, **overrides}
@pytest.fixture(autouse=True)
def oidc_settings(settings):
settings.GALTINN_EDITOR_GROUP = EDITOR_GROUP
settings.OIDC_RP_CLIENT_ID = "client-id"
def link(user, galtinn_id="42"):
return GaltinnUser.objects.create(user=user, galtinn_id=galtinn_id)
def editor(client, django_user_model, **kwargs):
user = django_user_model.objects.create_user(username="editor", password="pw", **kwargs)
user.user_permissions.add(Permission.objects.get(codename="access_admin"))
client.force_login(user)
return user
def test_linked_member_is_synced_from_claims():
user = User.objects.create_user(username="stale-name", email="stale@example.test")
link(user)
resolved = resolve_user(claims(preferred_username="kari-new"))
assert resolved.pk == user.pk
assert resolved.username == "kari-new"
assert resolved.email == "kari@neuf.test"
assert resolved.first_name == "Kari"
def test_linked_member_is_reactivated_and_added_to_editors():
user = User.objects.create_user(username="kari", is_active=False)
link(user)
resolved = resolve_user(claims())
assert resolved.is_active
assert resolved.groups.filter(name=WAGTAIL_EDITORS_GROUP).exists()
def test_linked_member_loses_local_password():
user = User.objects.create_user(username="kari", password="pw")
link(user)
resolved = resolve_user(claims())
assert not resolved.has_usable_password()
def test_integer_sub_matches_stored_id():
user = User.objects.create_user(username="kari")
link(user)
assert resolve_user(claims(sub=42)).pk == user.pk
def test_linked_username_collision_refuses_login(caplog):
linked = User.objects.create_user(username="kari-old", email="old@example.test")
link(linked)
other = User.objects.create_user(username="kari", email="other@example.test")
with caplog.at_level(logging.ERROR), pytest.raises(LoginRefused):
resolve_user(claims())
linked.refresh_from_db()
other.refresh_from_db()
assert linked.username == "kari-old"
assert linked.email == "old@example.test"
assert other.email == "other@example.test"
assert "resolve manually" in caplog.text
def test_linked_non_member_is_deactivated_and_loses_editors():
user = User.objects.create_user(username="kari-old", email="old@example.test")
user.groups.add(Group.objects.get(name=WAGTAIL_EDITORS_GROUP))
link(user)
with pytest.raises(LoginRefused):
resolve_user(claims(groups=["Styret"]))
user.refresh_from_db()
assert not user.is_active
assert not user.groups.exists()
assert user.username == "kari-old"
assert user.email == "old@example.test"
def test_linked_non_member_superuser_stays_active():
user = User.objects.create_superuser(username="kari", email="old@example.test", password="pw")
link(user)
with pytest.raises(LoginRefused):
resolve_user(claims(groups=[]))
user.refresh_from_db()
assert user.is_active
assert user.email == "old@example.test"
assert user.has_usable_password()
@pytest.mark.parametrize(
"local",
[
{"username": "kari", "email": "other@example.test"},
{"username": "KARI", "email": "other@example.test"},
{"username": "somebody", "email": "kari@neuf.test"},
{"username": "somebody", "email": "KARI@neuf.test"},
],
)
@pytest.mark.parametrize("groups", [[EDITOR_GROUP], []])
def test_unlinked_local_user_is_never_matched(local, groups, caplog):
user = User.objects.create_superuser(password="pw", **local)
with caplog.at_level(logging.ERROR), pytest.raises(LoginRefused):
resolve_user(claims(groups=groups))
user.refresh_from_db()
assert user.username == local["username"]
assert user.email == local["email"]
assert user.is_active
assert user.has_usable_password()
assert User.objects.count() == 1
assert not GaltinnUser.objects.exists()
assert ("link manually" in caplog.text) == bool(groups)
def test_link_is_never_rebound_to_another_galtinn_id():
user = User.objects.create_user(username="kari", email="kari@neuf.test")
link(user, galtinn_id="7")
with pytest.raises(LoginRefused):
resolve_user(claims())
assert GaltinnUser.objects.get(user=user).galtinn_id == "7"
def test_repeat_login_keeps_single_link():
resolve_user(claims())
resolve_user(claims())
assert GaltinnUser.objects.get(galtinn_id="42").user.username == "kari"
assert User.objects.count() == 1
def test_unknown_member_is_created_with_galtinn_identity():
resolved = resolve_user(claims())
assert resolved.username == "kari"
assert resolved.email == "kari@neuf.test"
assert resolved.first_name == "Kari"
assert not resolved.has_usable_password()
assert resolved.groups.filter(name=WAGTAIL_EDITORS_GROUP).exists()
assert GaltinnUser.objects.filter(user=resolved, galtinn_id="42").exists()
def test_unknown_non_member_is_not_created():
with pytest.raises(LoginRefused):
resolve_user(claims(groups=["Styret"]))
assert not User.objects.exists()
@pytest.mark.parametrize("name", ["sub", "preferred_username", "email"])
@pytest.mark.parametrize("value", [None, ""])
def test_missing_claim_refuses_login(name, value):
bad = claims(**{name: value}) if value is not None else claims()
if value is None:
del bad[name]
with pytest.raises(LoginRefused):
resolve_user(bad)
assert not User.objects.exists()
def test_empty_editor_group_setting_fails_closed(settings):
settings.GALTINN_EDITOR_GROUP = ""
with pytest.raises(LoginRefused):
resolve_user(claims())
assert not User.objects.exists()
def test_editors_group_missing_fails_loudly():
Group.objects.filter(name=WAGTAIL_EDITORS_GROUP).delete()
with pytest.raises(Group.DoesNotExist):
resolve_user(claims())
assert not User.objects.exists()
def test_login_redirects_to_galtinn_with_configured_callback(client, monkeypatch, settings):
settings.WAGTAIL_BASE_URL = "https://cms.example.test"
seen = {}
def authorize_redirect(request, redirect_uri):
seen["redirect_uri"] = redirect_uri
return HttpResponseRedirect("https://galtinn.example.test/authorize")
monkeypatch.setattr(oidc.oauth.galtinn, "authorize_redirect", authorize_redirect)
response = client.get(reverse("oidc_login"), {"next": "/admin/pages/3/"})
assert response.url == "https://galtinn.example.test/authorize"
assert seen["redirect_uri"] == "https://cms.example.test/oidc/callback/"
assert client.session["oidc_next"] == "/admin/pages/3/"
@pytest.mark.parametrize("setting", ["OIDC_RP_CLIENT_ID", "GALTINN_EDITOR_GROUP"])
def test_views_are_404_when_not_configured(client, settings, setting):
setattr(settings, setting, "")
assert client.get(reverse("oidc_login")).status_code == 404
assert client.get(reverse("oidc_callback")).status_code == 404
@pytest.mark.parametrize(
("next_url", "expected"),
[("/admin/pages/3/", "/admin/pages/3/"), ("https://evil.example.test/", "/admin/")],
)
def test_callback_only_follows_local_next(client, monkeypatch, next_url, expected):
monkeypatch.setattr(
oidc.oauth.galtinn, "authorize_access_token", lambda request: {"userinfo": claims()}
)
session = client.session
session["oidc_next"] = next_url
session.save()
response = client.get(reverse("oidc_callback"))
assert response.url == expected
@pytest.mark.parametrize(
"error", [OAuthError("denied"), JoseError("bad token"), requests.ConnectionError("down")]
)
def test_callback_flow_failure_redirects_to_login_with_message(client, monkeypatch, error):
def fail(request):
raise error
monkeypatch.setattr(oidc.oauth.galtinn, "authorize_access_token", fail)
response = client.get(reverse("oidc_callback"), follow=True)
assert response.redirect_chain[-1][0] == reverse("wagtailadmin_login")
assert any("feilet" in str(m) for m in response.context["messages"])
assert "_auth_user_id" not in client.session
def test_callback_without_userinfo_is_refused(client, monkeypatch):
monkeypatch.setattr(oidc.oauth.galtinn, "authorize_access_token", lambda request: {})
response = client.get(reverse("oidc_callback"), follow=True)
assert any("ikke tilgang" in str(m) for m in response.context["messages"])
assert "_auth_user_id" not in client.session
def test_callback_logs_in_and_redirects_to_admin(client, monkeypatch):
monkeypatch.setattr(
oidc.oauth.galtinn, "authorize_access_token", lambda request: {"userinfo": claims()}
)
response = client.get(reverse("oidc_callback"))
assert response.status_code == 302
assert response.url == reverse("wagtailadmin_home")
user = User.objects.get(username="kari")
assert client.session["_auth_user_id"] == str(user.pk)
def test_callback_refusal_redirects_to_login_with_message(client, monkeypatch):
monkeypatch.setattr(
oidc.oauth.galtinn,
"authorize_access_token",
lambda request: {"userinfo": claims(groups=[])},
)
response = client.get(reverse("oidc_callback"), follow=True)
assert response.redirect_chain[-1][0] == reverse("wagtailadmin_login")
messages = [str(m) for m in response.context["messages"]]
assert any("ikke tilgang" in m for m in messages)
assert "_auth_user_id" not in client.session
def test_login_page_shows_galtinn_button_once(client):
response = client.get(reverse("wagtailadmin_login"))
assert response.content.count(reverse("oidc_login").encode()) == 1
assert b"Logg inn med lokal bruker" in response.content
assert b"login-form" in response.content
def test_login_page_is_local_only_when_not_configured(client, settings):
settings.OIDC_RP_CLIENT_ID = ""
html = client.get(reverse("wagtailadmin_login")).content.decode()
assert reverse("oidc_login") not in html
assert "Logg inn med lokal bruker" not in html
assert re.search(r'<div class="local-login" id="local-login"\s*>', html)
def test_linked_account_page_shows_galtinn_note_and_disables_fields(client, django_user_model):
link(editor(client, django_user_model))
html = client.get(reverse("wagtailadmin_account")).content.decode()
assert "Navn og e-post må endres i Galtinn." in html
for field in ["first_name", "last_name", "email"]:
tag = re.search(rf'<input[^>]*name="name_email-{field}"[^>]*>', html).group(0)
assert "disabled" in tag
def test_linked_account_post_cannot_change_name_or_email(client, django_user_model):
user = editor(client, django_user_model, email="editor@neuf.test", first_name="Ed")
link(user)
client.post(
reverse("wagtailadmin_account"),
{"name_email-first_name": "Hacked", "name_email-email": "hacked@example.test"},
)
user.refresh_from_db()
assert user.first_name == "Ed"
assert user.email == "editor@neuf.test"
def test_local_account_page_keeps_editable_name_and_email(client, django_user_model):
editor(client, django_user_model)
html = client.get(reverse("wagtailadmin_account")).content.decode()
assert "Navn og e-post må endres i Galtinn." not in html
tag = re.search(r'<input[^>]*name="name_email-email"[^>]*>', html).group(0)
assert "disabled" not in tag
def test_account_page_requires_login(client):
response = client.get(reverse("wagtailadmin_account"))
assert response.status_code == 302
assert reverse("wagtailadmin_login") in response.url
+137 -1
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 5
requires-python = "==3.14.*"
[[package]]
@@ -29,6 +29,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" },
]
[[package]]
name = "authlib"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "joserfc" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/51/bc1729d3cfdc214b4935f4e886e4dd443c3065fd8e1e66423fe84b490f81/authlib-1.8.0.tar.gz", hash = "sha256:f3ecd5f1da737262fb53bf1a4d95c4ea1ad9dd509316587a255c99ab1838a4f0", size = 177759, upload-time = "2026-08-30T12:12:34.833Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/c6/6f124bcfbbfb20fba22c939b4e43a06dccfc0e1ca20e5634ca573cb1e271/authlib-1.8.0-py2.py3-none-any.whl", hash = "sha256:88aebbd9af6757e14e912d5dc007ae1dc1f3e27e3b2152ce7c552ee2c3b3c121", size = 260804, upload-time = "2026-08-30T12:12:33.162Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.15.0"
@@ -51,6 +64,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "cffi"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
{ url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
{ url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
{ url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
{ url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
{ url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
{ url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
{ url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
{ url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
{ url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
{ url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
{ url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.5.1"
@@ -161,6 +209,69 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" },
]
[[package]]
name = "cryptography"
version = "50.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" },
{ url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" },
{ url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
{ url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
{ url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
{ url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" },
{ url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
{ url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
{ url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
{ url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" },
{ url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" },
{ url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" },
{ url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" },
{ url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" },
{ url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" },
{ url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" },
{ url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" },
{ url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" },
{ url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" },
{ url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" },
{ url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" },
{ url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" },
{ url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
{ url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
{ url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
{ url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" },
{ url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
{ url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
{ url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
{ url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" },
{ url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
{ url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
{ url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
{ url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" },
{ url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
{ url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
{ url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
{ url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" },
{ url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
{ url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
@@ -313,10 +424,12 @@ name = "dnscms"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "authlib" },
{ name = "django" },
{ name = "django-extensions" },
{ name = "gunicorn" },
{ name = "psycopg2-binary" },
{ name = "requests" },
{ name = "wagtail" },
{ name = "wagtail-grapple" },
{ name = "wagtail-headless-preview" },
@@ -334,10 +447,12 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "authlib", specifier = ">=1.6,<2" },
{ name = "django", specifier = ">=6.1,<7" },
{ name = "django-extensions", specifier = ">=4.1,<5" },
{ name = "gunicorn", specifier = ">=26.2.0,<27" },
{ name = "psycopg2-binary", specifier = ">=2.9.12,<3" },
{ name = "requests", specifier = ">=2.34,<3" },
{ name = "wagtail", specifier = ">=8.0,<9" },
{ name = "wagtail-grapple", specifier = ">=0.31.0,<0.32" },
{ name = "wagtail-headless-preview", specifier = ">=0.9,<0.10" },
@@ -484,6 +599,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "joserfc"
version = "1.7.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/94/80fea1514b7c6d7d37804d3fe9ca81455f633347fc98731bd71ffe1faa17/joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea", size = 234990, upload-time = "2026-08-29T13:05:42.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/c5/82addfd375e5ee6520644e0553e4aadde92d668c4fc99cc716d337fe7bb3/joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159", size = 71269, upload-time = "2026-08-29T13:05:40.718Z" },
]
[[package]]
name = "laces"
version = "0.1.2"
@@ -623,6 +750,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
]
[[package]]
name = "pycparser"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" },
]
[[package]]
name = "pydantic"
version = "2.13.5"