Merge branch 'main' into dev

* main: (42 commits)
  website/docs: prepare 2024.4.2 release notes (#9555)
  web: bump the esbuild group in /web with 2 updates (#9616)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9611)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9560)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9563)
  ci: bump golangci/golangci-lint-action from 5 to 6 (#9615)
  web: bump esbuild from 0.20.2 to 0.21.0 in /web (#9617)
  core: bump cryptography from 42.0.6 to 42.0.7 (#9618)
  core: bump sentry-sdk from 2.0.1 to 2.1.1 (#9619)
  core: bump django from 5.0.4 to 5.0.5 (#9620)
  core, web: update translations (#9613)
  website/docs: move Sources from Integrations into Docs (#9515)
  website/docs: add procedurals to flow inspector docs (#9556)
  core: bump jinja2 from 3.1.3 to 3.1.4 (#9610)
  web: clean up the options rendering in PromptForm (#9564)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9608)
  website/docs: add instructions for deploying radius manually with docker compose (#9605)
  sources/scim: fix duplicate groups and invalid schema (#9466)
  core: fix condition in task clean_expiring_models (#9603)
  translate: Updates for file web/xliff/en.xlf in fr (#9600)
  ...
This commit is contained in:
Ken Sternberg
2024-05-07 08:49:15 -07:00
152 changed files with 7716 additions and 3127 deletions

View File

@ -12,7 +12,7 @@ should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v5
uses: golangci/golangci-lint-action@v6
with:
version: v1.54.2
args: --timeout 5000s --verbose

View File

@ -19,6 +19,7 @@ pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
-I .github/codespell-words.txt \
-S 'web/src/locales/**' \
-S 'website/developer-docs/api/reference/**' \
authentik \
internal \
cmd \

View File

@ -100,8 +100,6 @@ class SourceFlowManager:
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
existing_connections = self.connection_type.objects.filter(
@ -148,7 +146,6 @@ class SourceFlowManager:
]:
new_connection.user = user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,

View File

@ -2,7 +2,9 @@
from datetime import datetime, timedelta
from django.conf import ImproperlyConfigured
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
from django.core.cache import cache
from django.utils.timezone import now
from structlog.stdlib import get_logger
@ -15,6 +17,7 @@ from authentik.core.models import (
User,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -39,16 +42,31 @@ def clean_expired_models(self: SystemTask):
amount = 0
for session in AuthenticatedSession.objects.all():
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
match CONFIG.get("session_storage", "cache"):
case "cache":
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
case "db":
if not (
DBSessionStore.get_model_class()
.objects.filter(session_key=session.session_key, expire_date__gt=now())
.exists()
):
session.delete()
amount += 1
case _:
# Should never happen, as we check for other values in authentik/root/settings.py
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")

View File

@ -48,15 +48,21 @@ class TestSourceFlowManager(TestCase):
def test_authenticated_link(self):
"""Test authenticated user linking"""
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
user = User.objects.create(username="foo", email="foo@bar.baz")
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=user), self.identifier, {}
)
action, _ = flow_manager.get_action()
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
def test_unauthenticated_enroll_email(self):

View File

@ -2,11 +2,12 @@
from copy import deepcopy
from functools import partial
from typing import Any
from django.apps.registry import apps
from django.core.files import File
from django.db import connection
from django.db.models import Model
from django.db.models import ManyToManyRel, Model
from django.db.models.expressions import BaseExpression, Combinable
from django.db.models.signals import post_init
from django.http import HttpRequest
@ -44,7 +45,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
post_init.disconnect(dispatch_uid=request.request_id)
def serialize_simple(self, model: Model) -> dict:
"""Serialize a model in a very simple way. No ForeginKeys or other relationships are
"""Serialize a model in a very simple way. No ForeignKeys or other relationships are
resolved"""
data = {}
deferred_fields = model.get_deferred_fields()
@ -70,6 +71,9 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
for key, value in before.items():
if after.get(key) != value:
diff[key] = {"previous_value": value, "new_value": after.get(key)}
for key, value in after.items():
if key not in before and key not in diff and before.get(key) != value:
diff[key] = {"previous_value": before.get(key), "new_value": value}
return sanitize_item(diff)
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
@ -98,8 +102,37 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
thread_kwargs = {}
if hasattr(instance, "_previous_state") or created:
prev_state = getattr(instance, "_previous_state", {})
if created:
prev_state = {}
# Get current state
new_state = self.serialize_simple(instance)
diff = self.diff(prev_state, new_state)
thread_kwargs["diff"] = diff
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
def m2m_changed_handler( # noqa: PLR0913
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
pk_set: set[Any],
thread_kwargs: dict | None = None,
**_,
):
thread_kwargs = {}
m2m_field = None
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
_, _, action_direction = action.partition("_")
# resolve the "through" model to an actual field
for field in instance._meta.get_fields():
if not isinstance(field, ManyToManyRel):
continue
if field.through == sender:
m2m_field = field
if m2m_field:
# If we're clearing we just set the "flag" to True
if action_direction == "clear":
pk_set = True
thread_kwargs["diff"] = {m2m_field.related_name: {action_direction: pk_set}}
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)

View File

@ -1,9 +1,22 @@
from unittest.mock import PropertyMock, patch
from django.apps import apps
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.events.utils import sanitize_item
from authentik.lib.generators import generate_id
class TestEnterpriseAudit(TestCase):
class TestEnterpriseAudit(APITestCase):
"""Test audit middleware"""
def setUp(self) -> None:
self.user = create_test_admin_user()
def test_import(self):
"""Ensure middleware is imported when app.ready is called"""
@ -16,3 +29,182 @@ class TestEnterpriseAudit(TestCase):
self.assertIn(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware", settings.MIDDLEWARE
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_create(self):
"""Test create audit log"""
self.client.force_login(self.user)
username = generate_id()
response = self.client.post(
reverse("authentik_api:user-list"),
data={"name": generate_id(), "username": username, "groups": [], "path": "foo"},
)
user = User.objects.get(username=username)
self.assertEqual(response.status_code, 201)
events = Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": user.name,
"previous_value": None,
},
"path": {"new_value": "foo", "previous_value": None},
"type": {"new_value": "internal", "previous_value": None},
"uuid": {
"new_value": user.uuid.hex,
"previous_value": None,
},
"email": {"new_value": "", "previous_value": None},
"username": {
"new_value": user.username,
"previous_value": None,
},
"is_active": {"new_value": True, "previous_value": None},
"attributes": {"new_value": {}, "previous_value": None},
"date_joined": {
"new_value": sanitize_item(user.date_joined),
"previous_value": None,
},
"first_name": {"new_value": "", "previous_value": None},
"id": {"new_value": user.pk, "previous_value": None},
"last_name": {"new_value": "", "previous_value": None},
"password": {"new_value": "********************", "previous_value": None},
"password_change_date": {
"new_value": sanitize_item(user.password_change_date),
"previous_value": None,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_update(self):
"""Test update audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
current_name = user.name
new_name = generate_id()
response = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
data={"name": new_name},
)
user.refresh_from_db()
self.assertEqual(response.status_code, 200)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": new_name,
"previous_value": current_name,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_delete(self):
"""Test delete audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
response = self.client.delete(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_DELETED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertNotIn("diff", event.context)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_add(self):
"""Test m2m add audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-add-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"add": [user.pk]}},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_remove(self):
"""Test m2m remove audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-remove-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"remove": [user.pk]}},
)

View File

@ -60,6 +60,8 @@ class SystemTaskSerializer(ModelSerializer):
"duration",
"status",
"messages",
"expires",
"expiring",
]

View File

@ -214,7 +214,15 @@ class AuditMiddleware:
model=model_to_dict(instance),
).run()
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
def m2m_changed_handler(
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
thread_kwargs: dict | None = None,
**_,
):
"""Signal handler for all object's m2m_changed"""
if action not in ["pre_add", "pre_remove", "post_clear"]:
return
@ -229,4 +237,5 @@ class AuditMiddleware:
request,
user=user,
model=model_to_dict(instance),
**thread_kwargs,
).run()

View File

@ -119,7 +119,7 @@ class SystemTask(TenantTask):
"task_call_kwargs": sanitize_item(kwargs),
"status": self._status,
"messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours),
"expires": now() + timedelta(hours=self.result_timeout_hours + 3),
"expiring": True,
},
)

View File

@ -53,6 +53,7 @@ cache:
# result_backend:
# url: ""
# transport_options: ""
debug: false
remote_debug: false

View File

@ -41,7 +41,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Groups/{scim_group.id}")
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}")
scim_group.delete()
return response
@ -89,7 +89,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
for user in connections:
members.append(
GroupMember(
value=user.id,
value=user.scim_id,
)
)
if members:
@ -107,16 +107,19 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
exclude_unset=True,
),
)
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
def _update(self, group: Group, connection: SCIMGroup):
"""Update existing group"""
scim_group = self.to_scim(group)
scim_group.id = connection.id
scim_group.id = connection.scim_id
try:
return self._request(
"PUT",
f"/Groups/{scim_group.id}",
f"/Groups/{connection.scim_id}",
json=scim_group.model_dump(
mode="json",
exclude_unset=True,
@ -185,13 +188,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.add,
path="members",
@ -211,13 +214,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.remove,
path="members",

View File

@ -9,13 +9,14 @@ from pydanticscim.service_provider import (
)
from pydanticscim.user import User as BaseUser
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
class User(BaseUser):
"""Modified User schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:User",
]
schemas: list[str] = [SCIM_USER_SCHEMA]
externalId: str | None = None
meta: dict | None = None
@ -23,9 +24,7 @@ class User(BaseUser):
class Group(BaseGroup):
"""Modified Group schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:Group",
]
schemas: list[str] = [SCIM_GROUP_SCHEMA]
externalId: str | None = None
meta: dict | None = None

View File

@ -34,7 +34,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Users/{scim_user.id}")
response = self._request("DELETE", f"/Users/{scim_user.scim_id}")
scim_user.delete()
return response
@ -85,15 +85,18 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
exclude_unset=True,
),
)
SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
def _update(self, user: User, connection: SCIMUser):
"""Update existing user"""
scim_user = self.to_scim(user)
scim_user.id = connection.id
scim_user.id = connection.scim_id
self._request(
"PUT",
f"/Users/{connection.id}",
f"/Users/{connection.scim_id}",
json=scim_user.model_dump(
mode="json",
exclude_unset=True,

View File

@ -0,0 +1,76 @@
# Generated by Django 5.0.4 on 2024-05-03 12:38
import uuid
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.lib.migrations import progress_bar
def fix_scim_user_group_pk(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
SCIMUser = apps.get_model("authentik_providers_scim", "SCIMUser")
SCIMGroup = apps.get_model("authentik_providers_scim", "SCIMGroup")
db_alias = schema_editor.connection.alias
print("\nFixing primary key for SCIM users, this might take a couple of minutes...")
for user in progress_bar(SCIMUser.objects.using(db_alias).all()):
SCIMUser.objects.using(db_alias).filter(
pk=user.pk, user=user.user_id, provider=user.provider_id
).update(scim_id=user.pk, id=uuid.uuid4())
print("\nFixing primary key for SCIM groups, this might take a couple of minutes...")
for group in progress_bar(SCIMGroup.objects.using(db_alias).all()):
SCIMGroup.objects.using(db_alias).filter(
pk=group.pk, group=group.group_id, provider=group.provider_id
).update(scim_id=group.pk, id=uuid.uuid4())
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_scim",
"0001_squashed_0006_rename_parent_group_scimprovider_filter_group",
),
]
operations = [
migrations.AddField(
model_name="scimgroup",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.AddField(
model_name="scimuser",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.RunPython(fix_scim_user_group_pk),
migrations.AlterField(
model_name="scimgroup",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="scimuser",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(model_name="scimuser", name="scim_id", field=models.TextField()),
migrations.AlterField(model_name="scimgroup", name="scim_id", field=models.TextField()),
migrations.AlterUniqueTogether(
name="scimgroup",
unique_together={("scim_id", "group", "provider")},
),
migrations.AlterUniqueTogether(
name="scimuser",
unique_together={("scim_id", "user", "provider")},
),
]

View File

@ -1,5 +1,7 @@
"""SCIM Provider models"""
from uuid import uuid4
from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet
@ -97,12 +99,13 @@ class SCIMMapping(PropertyMapping):
class SCIMUser(models.Model):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "user", "provider"),)
unique_together = (("scim_id", "user", "provider"),)
def __str__(self) -> str:
return f"SCIM User {self.user_id} to {self.provider_id}"
@ -111,12 +114,13 @@ class SCIMUser(models.Model):
class SCIMGroup(models.Model):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "group", "provider"),)
unique_together = (("scim_id", "group", "provider"),)
def __str__(self) -> str:
return f"SCIM Group {self.group_id} to {self.provider_id}"

View File

@ -88,6 +88,72 @@ class SCIMUserTests(TestCase):
},
)
@Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen
to return the same object ID"""
# Create duplicate provider
provider: SCIMProvider = SCIMProvider.objects.create(
name=generate_id(),
url="https://localhost",
token=generate_id(),
exclude_users_service_account=True,
)
app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
app.backchannel_providers.add(provider)
provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
provider.property_mappings_group.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
)
scim_id = generate_id()
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 4)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@Mocker()
def test_user_create_update(self, mock: Mocker):
"""Test user creation and update"""

View File

@ -376,7 +376,13 @@ CELERY = {
"task_default_queue": "authentik",
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
"broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
"broker_transport_options": CONFIG.get_dict_from_b64_json(
"broker.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"result_backend_transport_options": CONFIG.get_dict_from_b64_json(
"result_backend.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"redis_retry_on_timeout": True,
}
# Sentry integration

View File

@ -13,6 +13,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.models import Group, User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
from authentik.sources.scim.models import SCIMSourceGroup
from authentik.sources.scim.views.v2.base import SCIMView
@ -26,9 +27,11 @@ class GroupsView(SCIMView):
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
"""Convert Group to SCIM data"""
payload = SCIMGroupModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_group.group.pk),
externalId=scim_group.id,
displayName=scim_group.group.name,
members=[],
meta={
"resourceType": "Group",
"location": self.request.build_absolute_uri(
@ -42,28 +45,24 @@ class GroupsView(SCIMView):
),
},
)
return payload.model_dump(
mode="json",
exclude_unset=True,
)
for member in scim_group.group.users.order_by("pk"):
member: User
payload.members.append(GroupMember(value=str(member.uuid)))
return payload.model_dump(mode="json", exclude_unset=True)
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
"""List Group handler"""
base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related(
"group__users"
)
if group_id:
connection = (
SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id)
.select_related("group")
.first()
)
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first()
if not connection:
raise Http404
return Response(self.group_to_scim(connection))
connections = (
SCIMSourceGroup.objects.filter(source=self.source)
.select_related("group")
.order_by("pk")
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request))
)
connections = connections.filter(self.filter_parse(request))
page = self.paginate_query(connections)
return Response(
{
@ -79,6 +78,8 @@ class GroupsView(SCIMView):
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
"""Partial update a group"""
group = connection.group if connection else Group()
if _group := Group.objects.filter(name=data.get("displayName")).first():
group = _group
if "displayName" in data:
group.name = data.get("displayName")
if group.name == "":

View File

@ -11,6 +11,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.models import User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import User as SCIMUserModel
from authentik.sources.scim.models import SCIMSourceUser
from authentik.sources.scim.views.v2.base import SCIMView
@ -33,6 +34,7 @@ class UsersView(SCIMView):
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
"""Convert User to SCIM data"""
payload = SCIMUserModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_user.user.uuid),
externalId=scim_user.id,
userName=scim_user.user.username,
@ -62,10 +64,7 @@ class UsersView(SCIMView):
),
},
)
final_payload = payload.model_dump(
mode="json",
exclude_unset=True,
)
final_payload = payload.model_dump(mode="json", exclude_unset=True)
final_payload.update(scim_user.attributes)
return final_payload
@ -99,6 +98,8 @@ class UsersView(SCIMView):
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
"""Partial update a user"""
user = connection.user if connection else User()
if _user := User.objects.filter(username=data.get("userName")).first():
user = _user
user.path = self.source.get_user_path()
if "userName" in data:
user.username = data.get("userName")

View File

@ -3,6 +3,7 @@
from tenant_schemas_celery.scheduler import (
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
)
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@ -11,3 +12,11 @@ class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@classmethod
def get_queryset(cls):
return super().get_queryset().filter(ready=True)
def apply_entry(self, entry: TenantAwareScheduleEntry, producer=None):
# https://github.com/maciej-gol/tenant-schemas-celery/blob/master/tenant_schemas_celery/scheduler.py#L85
# When (as by default) no tenant schemas are set, the public schema is excluded
# so we need to explicitly include it here, otherwise the task is not executed
if entry.tenant_schemas is None:
entry.tenant_schemas = self.get_queryset().values_list("schema_name", flat=True)
return super().apply_entry(entry, producer)

6
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
@ -28,9 +28,9 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024041.2
goauthentik.io/api/v3 v3.2024041.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0
golang.org/x/oauth2 v0.20.0
golang.org/x/sync v0.7.0
gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab

12
go.sum
View File

@ -111,8 +111,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024041.2 h1:gbquIA8RU+9jJbFdGckQTtJzOfWVp2+QdF4LuNVTAWM=
goauthentik.io/api/v3 v3.2024041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024041.3 h1:p5X3zNIm+a/gnl9pyALNCwHkeg0HYIS83B5Xh6vyhtQ=
goauthentik.io/api/v3 v3.2024041.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -382,8 +382,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -192,7 +192,9 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
})
})
mux.HandleFunc("/outpost.goauthentik.io/start", a.handleAuthStart)
mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) {
a.handleAuthStart(w, r, "")
})
mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
switch *p.Mode {

View File

@ -59,19 +59,11 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
a.log.Trace("path can be accessed without authentication")
return
}
a.handleAuthStart(rw, r)
// set the redirect flag to the current URL we have, since we redirect
// to a (possibly) different domain, but we want to be redirected back
// to the application
// X-Forwarded-Uri is only the path, so we need to build the entire URL
s, _ := a.sessions.Get(r, a.SessionName())
if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet {
s.Values[constants.SessionRedirect] = fwd.String()
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
}
}
a.handleAuthStart(rw, r, fwd.String())
}
func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request) {
@ -110,19 +102,11 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request
a.log.Trace("path can be accessed without authentication")
return
}
a.handleAuthStart(rw, r)
// set the redirect flag to the current URL we have, since we redirect
// to a (possibly) different domain, but we want to be redirected back
// to the application
// X-Forwarded-Uri is only the path, so we need to build the entire URL
s, _ := a.sessions.Get(r, a.SessionName())
if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet {
s.Values[constants.SessionRedirect] = fwd.String()
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
}
}
a.handleAuthStart(rw, r, fwd.String())
}
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
@ -185,17 +169,9 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request
a.log.Trace("path can be accessed without authentication")
return
}
a.handleAuthStart(rw, r)
// set the redirect flag to the current URL we have, since we redirect
// to a (possibly) different domain, but we want to be redirected back
// to the application
// X-Forwarded-Uri is only the path, so we need to build the entire URL
s, _ := a.sessions.Get(r, a.SessionName())
if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet {
s.Values[constants.SessionRedirect] = fwd.String()
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
}
}
a.handleAuthStart(rw, r, fwd.String())
}

View File

@ -47,16 +47,14 @@ func TestForwardHandleCaddy_Single_Headers(t *testing.T) {
a.forwardHandleCaddy(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://test.goauthentik.io/app", st.Redirect)
}
func TestForwardHandleCaddy_Single_Claims(t *testing.T) {
@ -134,14 +132,12 @@ func TestForwardHandleCaddy_Domain_Header(t *testing.T) {
a.forwardHandleCaddy(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://test.goauthentik.io/app", st.Redirect)
}

View File

@ -32,16 +32,14 @@ func TestForwardHandleEnvoy_Single_Headers(t *testing.T) {
a.forwardHandleEnvoy(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://ext.t.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://ext.t.goauthentik.io/app", st.Redirect)
}
func TestForwardHandleEnvoy_Single_Claims(t *testing.T) {
@ -102,15 +100,13 @@ func TestForwardHandleEnvoy_Domain_Header(t *testing.T) {
a.forwardHandleEnvoy(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://test.goauthentik.io/app", st.Redirect)
}

View File

@ -47,16 +47,14 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://test.goauthentik.io/app", st.Redirect)
}
func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
@ -134,14 +132,12 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
s, _ := a.sessions.Get(req, a.SessionName())
loc, st := a.assertState(t, req, rr)
shouldUrl := url.Values{
"client_id": []string{*a.proxyConfig.ClientId},
"redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
"response_type": []string{"code"},
"state": []string{s.Values[constants.SessionOAuthState].(string)},
}
assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
assert.Equal(t, "http://test.goauthentik.io/app", st.Redirect)
}

View File

@ -1,13 +1,10 @@
package application
import (
"encoding/base64"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/securecookie"
"goauthentik.io/api/v3"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
@ -48,69 +45,59 @@ func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
return u.String(), true
}
func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request) {
newState := base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
state, err := a.createState(r, fwd)
if err != nil {
a.log.WithError(err).Warning("failed to create state")
return
}
s, _ := a.sessions.Get(r, a.SessionName())
// Check if we already have a state in the session,
// and if we do we don't do anything here
currentState, ok := s.Values[constants.SessionOAuthState].(string)
if ok {
claims, err := a.checkAuth(rw, r)
if err != nil && claims != nil {
a.log.Trace("auth start request with existing authenticated session")
a.redirect(rw, r)
return
}
a.log.Trace("session already has state, sending redirect to current state")
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(currentState), http.StatusFound)
return
}
rd, ok := a.checkRedirectParam(r)
if ok {
s.Values[constants.SessionRedirect] = rd
a.log.WithField("rd", rd).Trace("Setting redirect")
}
s.Values[constants.SessionOAuthState] = newState
err := s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
}
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound)
}
func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) {
s, err := a.sessions.Get(r, a.SessionName())
if err != nil {
a.log.WithError(err).Trace("failed to get session")
}
state, ok := s.Values[constants.SessionOAuthState]
if !ok {
a.log.Warning("No state saved in session")
a.redirect(rw, r)
return
}
claims, err := a.redeemCallback(state.(string), r.URL, r.Context())
if err != nil {
a.log.WithError(err).Warning("failed to redeem code")
rw.WriteHeader(400)
// To prevent the user from just refreshing and cause more errors, delete
// the state from the session
delete(s.Values, constants.SessionOAuthState)
err := s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
rw.WriteHeader(400)
return
}
return
}
s.Options.MaxAge = int(time.Until(time.Unix(int64(claims.Exp), 0)).Seconds())
s.Values[constants.SessionClaims] = &claims
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
rw.WriteHeader(400)
return
}
a.redirect(rw, r)
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound)
}
func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
s, err := a.sessions.Get(r, a.SessionName())
if err != nil {
a.log.WithError(err).Warning("failed to decode session")
}
if r.Header.Get(constants.HeaderAuthorization) != "" && *a.proxyConfig.InterceptHeaderAuth {
rw.WriteHeader(401)
er := a.errorTemplates.Execute(rw, ErrorPageData{
Title: "Unauthenticated",
Message: "Due to 'Receive header authentication' being set, no redirect is performed.",
ProxyPrefix: "/outpost.goauthentik.io",
})
if er != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
}
}
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")
// In forward_domain we only check that the current URL's host
// ends with the cookie domain (remove the leading period if set)
if !strings.HasSuffix(r.URL.Hostname(), dom) {
a.log.WithField("url", r.URL.String()).WithField("cd", dom).Warning("Invalid redirect found")
redirectUrl = a.proxyConfig.ExternalHost
}
}
if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet {
s.Values[constants.SessionRedirect] = redirectUrl
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
}
}
urlArgs := url.Values{
redirectParam: []string{redirectUrl},
}
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/outpost.goauthentik.io/start")
http.Redirect(rw, r, authUrl+"?"+urlArgs.Encode(), http.StatusFound)
}

View File

@ -3,22 +3,43 @@ package application
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/proxyv2/constants"
"golang.org/x/oauth2"
)
func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Context) (*Claims, error) {
state := u.Query().Get("state")
a.log.WithFields(log.Fields{
"states": savedState,
"expected": state,
}).Trace("tracing states")
if savedState != state {
return nil, fmt.Errorf("invalid state")
func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) {
state := a.stateFromRequest(r)
if state == nil {
a.log.Warning("invalid state")
a.redirect(rw, r)
return
}
claims, err := a.redeemCallback(r.URL, r.Context())
if err != nil {
a.log.WithError(err).Warning("failed to redeem code")
a.redirect(rw, r)
return
}
s, err := a.sessions.Get(r, a.SessionName())
if err != nil {
a.log.WithError(err).Trace("failed to get session")
}
s.Options.MaxAge = int(time.Until(time.Unix(int64(claims.Exp), 0)).Seconds())
s.Values[constants.SessionClaims] = &claims
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session")
rw.WriteHeader(400)
return
}
a.redirect(rw, r)
}
func (a *Application) redeemCallback(u *url.URL, c context.Context) (*Claims, error) {
code := u.Query().Get("code")
if code == "" {
return nil, fmt.Errorf("blank code")

View File

@ -0,0 +1,95 @@
package application
import (
"encoding/base32"
"encoding/base64"
"fmt"
"net/http"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/securecookie"
"github.com/mitchellh/mapstructure"
)
type OAuthState struct {
Issuer string `json:"iss" mapstructure:"iss"`
SessionID string `json:"sid" mapstructure:"sid"`
State string `json:"state" mapstructure:"state"`
Redirect string `json:"redirect" mapstructure:"redirect"`
}
func (oas *OAuthState) GetExpirationTime() (*jwt.NumericDate, error) { return nil, nil }
func (oas *OAuthState) GetIssuedAt() (*jwt.NumericDate, error) { return nil, nil }
func (oas *OAuthState) GetNotBefore() (*jwt.NumericDate, error) { return nil, nil }
func (oas *OAuthState) GetIssuer() (string, error) { return oas.Issuer, nil }
func (oas *OAuthState) GetSubject() (string, error) { return oas.State, nil }
func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error) { return nil, nil }
var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
func (a *Application) createState(r *http.Request, fwd string) (string, error) {
s, _ := a.sessions.Get(r, a.SessionName())
if s.ID == "" {
// Ensure session has an ID
s.ID = base32RawStdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
}
st := &OAuthState{
Issuer: fmt.Sprintf("goauthentik.io/outpost/%s", a.proxyConfig.GetClientId()),
State: base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)),
SessionID: s.ID,
Redirect: fwd,
}
if fwd == "" {
// This should only really be hit for nginx forward_auth
// as for that the auth start redirect URL is generated by the
// reverse proxy, and as such we won't have a request we just
// denied to reference for final URL
rd, ok := a.checkRedirectParam(r)
if ok {
a.log.WithField("rd", rd).Trace("Setting redirect")
st.Redirect = rd
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, st)
tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret()))
if err != nil {
return "", err
}
return tokenString, nil
}
func (a *Application) stateFromRequest(r *http.Request) *OAuthState {
stateJwt := r.URL.Query().Get("state")
token, err := jwt.Parse(stateJwt, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(a.proxyConfig.GetCookieSecret()), nil
})
if err != nil {
a.log.WithError(err).Warning("failed to parse state jwt")
return nil
}
iss, err := token.Claims.GetIssuer()
if err != nil {
a.log.WithError(err).Warning("state jwt without issuer")
return nil
}
if iss != fmt.Sprintf("goauthentik.io/outpost/%s", a.proxyConfig.GetClientId()) {
a.log.WithField("issuer", iss).Warning("invalid state jwt issuer")
return nil
}
claims := &OAuthState{}
err = mapstructure.Decode(token.Claims, &claims)
if err != nil {
a.log.WithError(err).Warning("failed to mapdecode")
return nil
}
s, _ := a.sessions.Get(r, a.SessionName())
if claims.SessionID != s.ID {
a.log.WithField("is", claims.SessionID).WithField("should", s.ID).Warning("mismatched session ID")
return nil
}
return claims
}

View File

@ -2,6 +2,9 @@ package application
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"goauthentik.io/api/v3"
"goauthentik.io/internal/outpost/ak"
@ -45,11 +48,11 @@ func newTestApplication() *Application {
Name: ak.TestSecret(),
ClientId: api.PtrString(ak.TestSecret()),
ClientSecret: api.PtrString(ak.TestSecret()),
CookieDomain: api.PtrString(""),
CookieSecret: api.PtrString(ak.TestSecret()),
ExternalHost: "https://ext.t.goauthentik.io",
InternalHost: api.PtrString("http://backend"),
InternalHostSslValidation: api.PtrBool(true),
CookieDomain: api.PtrString(""),
Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(),
SkipPathRegex: api.PtrString("/skip.*"),
BasicAuthEnabled: api.PtrBool(true),
@ -67,3 +70,25 @@ func newTestApplication() *Application {
ts.apps = append(ts.apps, a)
return a
}
func (a *Application) assertState(t *testing.T, req *http.Request, response *httptest.ResponseRecorder) (*url.URL, *OAuthState) {
loc, _ := response.Result().Location()
q := loc.Query()
state := q.Get("state")
a.log.WithField("actual", state).Warning("actual state")
// modify request to set state so we can parse it
nr := req.Clone(req.Context())
nrq := nr.URL.Query()
nrq.Set("state", state)
nr.URL.RawQuery = nrq.Encode()
// parse state
parsed := a.stateFromRequest(nr)
if parsed == nil {
panic("Could not parse state")
}
// Remove state from URL
q.Del("state")
loc.RawQuery = q.Encode()
return loc, parsed
}

View File

@ -4,10 +4,6 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"goauthentik.io/api/v3"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
func urlJoin(originalUrl string, newPath string) string {
@ -18,62 +14,18 @@ func urlJoin(originalUrl string, newPath string) string {
return u
}
func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
s, err := a.sessions.Get(r, a.SessionName())
if err != nil {
a.log.WithError(err).Warning("failed to decode session")
}
if r.Header.Get(constants.HeaderAuthorization) != "" && *a.proxyConfig.InterceptHeaderAuth {
rw.WriteHeader(401)
er := a.errorTemplates.Execute(rw, ErrorPageData{
Title: "Unauthenticated",
Message: "Due to 'Receive header authentication' being set, no redirect is performed.",
ProxyPrefix: "/outpost.goauthentik.io",
})
if er != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
}
}
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")
// In forward_domain we only check that the current URL's host
// ends with the cookie domain (remove the leading period if set)
if !strings.HasSuffix(r.URL.Hostname(), dom) {
a.log.WithField("url", r.URL.String()).WithField("cd", dom).Warning("Invalid redirect found")
redirectUrl = a.proxyConfig.ExternalHost
}
}
if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet {
s.Values[constants.SessionRedirect] = redirectUrl
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
}
}
urlArgs := url.Values{
redirectParam: []string{redirectUrl},
}
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/outpost.goauthentik.io/start")
http.Redirect(rw, r, authUrl+"?"+urlArgs.Encode(), http.StatusFound)
}
func (a *Application) redirect(rw http.ResponseWriter, r *http.Request) {
redirect := a.proxyConfig.ExternalHost
s, _ := a.sessions.Get(r, a.SessionName())
redirectR, ok := s.Values[constants.SessionRedirect]
if ok {
redirect = redirectR.(string)
fallbackRedirect := a.proxyConfig.ExternalHost
state := a.stateFromRequest(r)
if state == nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
rd, ok := a.checkRedirectParam(r)
if ok {
redirect = rd
if state.Redirect == "" {
state.Redirect = fallbackRedirect
}
a.log.WithField("redirect", redirect).Trace("final redirect")
http.Redirect(rw, r, redirect, http.StatusFound)
a.log.WithField("redirect", state.Redirect).Trace("final redirect")
http.Redirect(rw, r, state.Redirect, http.StatusFound)
}
// toString Generic to string function, currently supports actual strings and integers

View File

@ -3,9 +3,10 @@ package hs256
import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
)
type KeySet struct {
@ -15,17 +16,23 @@ type KeySet struct {
func NewKeySet(secret string) *KeySet {
return &KeySet{
m: jwt.GetSigningMethod("HS256"),
m: jwt.SigningMethodHS256,
secret: secret,
}
}
func (ks *KeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
parts := strings.Split(jwt, ".")
err := ks.m.Verify(strings.Join(parts[0:2], "."), parts[2], []byte(ks.secret))
func (ks *KeySet) VerifySignature(ctx context.Context, rawJWT string) ([]byte, error) {
_, err := jwt.Parse(rawJWT, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(ks.secret), nil
})
if err != nil {
return nil, err
}
parts := strings.Split(rawJWT, ".")
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
return payload, err
}

View File

@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2024\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -410,6 +410,14 @@ msgstr "Vous êtes sur le point de vous connecter à %(application)s."
msgid "Subject-alt name"
msgstr "Nom alternatif subject"
#: authentik/crypto/builder.py
msgid "rsa"
msgstr "rsa"
#: authentik/crypto/builder.py
msgid "ecdsa"
msgstr "ecdsa"
#: authentik/crypto/models.py
msgid "PEM-encoded Certificate data"
msgstr "Données du certificat au format PEM"
@ -1740,6 +1748,22 @@ msgstr "RSA-SHA384"
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA1"
msgstr "ECDSA-SHA1"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA256"
msgstr "ECDSA-SHA256"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA384"
msgstr "ECDSA-SHA384"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA512"
msgstr "ECDSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "DSA-SHA1"
msgstr "DSA-SHA1"

View File

@ -16,7 +16,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Anton Babenko, 2024\n"
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
@ -405,6 +405,14 @@ msgstr "Вы собираетесь войти в %(application)s."
msgid "Subject-alt name"
msgstr "Альтернативное имя субъекта"
#: authentik/crypto/builder.py
msgid "rsa"
msgstr "rsa"
#: authentik/crypto/builder.py
msgid "ecdsa"
msgstr "ecdsa"
#: authentik/crypto/models.py
msgid "PEM-encoded Certificate data"
msgstr "Данные сертификата, закодированные в формате PEM"
@ -1721,6 +1729,22 @@ msgstr "RSA-SHA384"
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA1"
msgstr "ECDSA-SHA1"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA256"
msgstr "ECDSA-SHA256"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA384"
msgstr "ECDSA-SHA384"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA512"
msgstr "ECDSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "DSA-SHA1"
msgstr "DSA-SHA1"
@ -2005,11 +2029,12 @@ msgstr "Ссылка авторизации"
#: authentik/sources/oauth/models.py
msgid "URL the user is redirect to to conest the flow."
msgstr "URL, на который перенаправляется пользователь для авторизации."
msgstr ""
"URL, на который пользователь перенаправляется для согласования потока."
#: authentik/sources/oauth/models.py
msgid "Access Token URL"
msgstr "Ссылка ключа доступа"
msgstr "Ссылка Access токена"
#: authentik/sources/oauth/models.py
msgid "URL used by authentik to retrieve tokens."
@ -2922,7 +2947,7 @@ msgstr "Неверный пароль"
#: authentik/stages/prompt/models.py
msgid "Text: Simple Text input"
msgstr "Текст: простой ввод текста"
msgstr "Текст: простой текстовый ввод"
#: authentik/stages/prompt/models.py
msgid "Text area: Multiline Text Input."
@ -2931,14 +2956,14 @@ msgstr "Текстовая область: Многострочный текст
#: authentik/stages/prompt/models.py
msgid "Text (read-only): Simple Text input, but cannot be edited."
msgstr ""
"Текст (только для чтения): Простой ввод текста, который нельзя "
"Текст (только для чтения): Простой текстовый ввод, который нельзя "
"редактировать."
#: authentik/stages/prompt/models.py
msgid "Text area (read-only): Multiline Text input, but cannot be edited."
msgstr ""
"Текстовая область (только для чтения): Многострочный ввод текста, но не "
"может быть отредактирован."
"Текстовая область (только для чтения): Многострочный текстовый ввод, который"
" нельзя редактировать."
#: authentik/stages/prompt/models.py
msgid ""
@ -3152,7 +3177,7 @@ msgstr ""
#: authentik/tenants/models.py
msgid "Enable the ability for users to change their username."
msgstr ""
"Включить возможность изменения пользователями своего имени пользователя"
"Включение возможности изменения пользователями своего имени пользователя"
#: authentik/tenants/models.py
msgid ""

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -379,6 +379,14 @@ msgstr "您即将登录 %(application)s。"
msgid "Subject-alt name"
msgstr "替代名称"
#: authentik/crypto/builder.py
msgid "rsa"
msgstr "rsa"
#: authentik/crypto/builder.py
msgid "ecdsa"
msgstr "ecdsa"
#: authentik/crypto/models.py
msgid "PEM-encoded Certificate data"
msgstr "PEM 编码的证书数据"
@ -1580,6 +1588,22 @@ msgstr "RSA-SHA384"
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA1"
msgstr "ECDSA-SHA1"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA256"
msgstr "ECDSA-SHA256"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA384"
msgstr "ECDSA-SHA384"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA512"
msgstr "ECDSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "DSA-SHA1"
msgstr "DSA-SHA1"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
"POT-Creation-Date: 2024-05-03 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -379,6 +379,14 @@ msgstr "您即将登录 %(application)s。"
msgid "Subject-alt name"
msgstr "替代名称"
#: authentik/crypto/builder.py
msgid "rsa"
msgstr "rsa"
#: authentik/crypto/builder.py
msgid "ecdsa"
msgstr "ecdsa"
#: authentik/crypto/models.py
msgid "PEM-encoded Certificate data"
msgstr "PEM 编码的证书数据"
@ -1580,6 +1588,22 @@ msgstr "RSA-SHA384"
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA1"
msgstr "ECDSA-SHA1"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA256"
msgstr "ECDSA-SHA256"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA384"
msgstr "ECDSA-SHA384"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "ECDSA-SHA512"
msgstr "ECDSA-SHA512"
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
msgid "DSA-SHA1"
msgstr "DSA-SHA1"

235
poetry.lock generated
View File

@ -920,63 +920,63 @@ files = [
[[package]]
name = "coverage"
version = "7.5.0"
version = "7.5.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"},
{file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"},
{file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"},
{file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"},
{file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"},
{file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"},
{file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"},
{file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"},
{file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"},
{file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"},
{file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"},
{file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"},
{file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"},
{file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"},
{file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"},
{file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"},
{file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"},
{file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"},
{file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"},
{file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"},
{file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"},
{file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"},
{file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
{file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
{file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"},
{file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"},
{file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"},
{file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"},
{file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"},
{file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"},
{file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"},
{file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"},
{file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"},
{file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"},
{file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"},
{file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"},
{file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"},
{file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"},
{file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"},
{file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"},
{file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"},
{file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"},
{file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"},
{file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"},
{file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"},
{file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"},
{file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"},
{file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"},
{file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"},
{file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"},
{file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"},
{file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"},
{file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"},
{file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"},
{file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"},
{file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"},
{file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"},
{file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"},
{file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"},
{file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"},
{file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"},
{file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"},
{file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"},
{file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"},
{file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"},
{file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"},
{file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"},
{file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"},
{file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"},
{file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"},
{file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"},
{file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"},
{file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"},
{file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"},
]
[package.extras]
@ -984,43 +984,43 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "42.0.5"
version = "42.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"},
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"},
{file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"},
{file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"},
{file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"},
{file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"},
{file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"},
{file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"},
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"},
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"},
{file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"},
{file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"},
{file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"},
{file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"},
{file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"},
{file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"},
]
[package.dependencies]
@ -1123,13 +1123,13 @@ files = [
[[package]]
name = "django"
version = "5.0.4"
version = "5.0.5"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
{file = "Django-5.0.4-py3-none-any.whl", hash = "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"},
{file = "Django-5.0.4.tar.gz", hash = "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd"},
{file = "Django-5.0.5-py3-none-any.whl", hash = "sha256:8af4f166dc9a2bb822f9374cd78e34a10c286b402597fe2c7fb97c131656ba65"},
{file = "Django-5.0.5.tar.gz", hash = "sha256:dc95c9cb2a37ba54599d9d1c8faf81609d36f3e74cd04395ce1300573e57baf9"},
]
[package.dependencies]
@ -1217,13 +1217,13 @@ hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"]
[[package]]
name = "django-storages"
version = "1.14.2"
version = "1.14.3"
description = "Support for many storage backends in Django"
optional = false
python-versions = ">=3.7"
files = [
{file = "django-storages-1.14.2.tar.gz", hash = "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5"},
{file = "django_storages-1.14.2-py3-none-any.whl", hash = "sha256:1db759346b52ada6c2efd9f23d8241ecf518813eb31db9e2589207174f58f6ad"},
{file = "django-storages-1.14.3.tar.gz", hash = "sha256:95a12836cd998d4c7a4512347322331c662d9114c4344f932f5e9c0fce000608"},
{file = "django_storages-1.14.3-py3-none-any.whl", hash = "sha256:31f263389e95ce3a1b902fb5f739a7ed32895f7d8b80179fe7453ecc0dfe102e"},
]
[package.dependencies]
@ -1788,13 +1788,13 @@ files = [
[[package]]
name = "jinja2"
version = "3.1.3"
version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
@ -3390,28 +3390,28 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.4.2"
version = "0.4.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"},
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"},
{file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"},
{file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"},
{file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"},
{file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"},
{file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
{file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
{file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
{file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
{file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
{file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
]
[[package]]
@ -3468,13 +3468,13 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]]
name = "sentry-sdk"
version = "2.0.1"
version = "2.1.1"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
{file = "sentry_sdk-2.0.1-py2.py3-none-any.whl", hash = "sha256:b54c54a2160f509cf2757260d0cf3885b608c6192c2555a3857e3a4d0f84bdb3"},
{file = "sentry_sdk-2.0.1.tar.gz", hash = "sha256:c278e0f523f6f0ee69dc43ad26dcdb1202dffe5ac326ae31472e012d941bee21"},
{file = "sentry_sdk-2.1.1-py2.py3-none-any.whl", hash = "sha256:99aeb78fb76771513bd3b2829d12613130152620768d00cd3e45ac00cb17950f"},
{file = "sentry_sdk-2.1.1.tar.gz", hash = "sha256:95d8c0bb41c8b0bc37ab202c2c4a295bb84398ee05f4cdce55051cd75b926ec1"},
]
[package.dependencies]
@ -3483,6 +3483,7 @@ urllib3 = ">=1.26.11"
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
anthropic = ["anthropic (>=0.16)"]
arq = ["arq (>=0.23)"]
asyncpg = ["asyncpg (>=0.23)"]
beam = ["apache-beam (>=2.12)"]
@ -3498,6 +3499,8 @@ flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
grpcio = ["grpcio (>=1.21.1)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
huggingface-hub = ["huggingface-hub (>=0.22)"]
langchain = ["langchain (>=0.0.210)"]
loguru = ["loguru (>=0.5)"]
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]

View File

@ -44167,6 +44167,12 @@ components:
type: array
items:
$ref: '#/components/schemas/LogEvent'
expires:
type: string
format: date-time
nullable: true
expiring:
type: boolean
required:
- description
- duration

2903
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@
"build": "run-s build-locales esbuild:build",
"build-proxy": "run-s build-locales esbuild:build-proxy",
"watch": "run-s build-locales esbuild:watch",
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=16384' eslint . --max-warnings 0 --fix",
"lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=16384' node scripts/eslint-precommit.mjs",
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=65536' eslint . --max-warnings 0 --fix",
"lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=65536' node scripts/eslint-precommit.mjs",
"lint:spelling": "node scripts/check-spelling.mjs",
"lit-analyse": "lit-analyzer src",
"precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.1-1714655911",
"@goauthentik/api": "^2024.4.1-1714957644",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1",
@ -81,13 +81,13 @@
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.17",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-essentials": "^8.0.10",
"@storybook/addon-links": "^8.0.10",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.0.9",
"@storybook/web-components": "^8.0.9",
"@storybook/web-components-vite": "^8.0.9",
"@storybook/manager-api": "^8.0.10",
"@storybook/web-components": "^8.0.10",
"@storybook/web-components-vite": "^8.0.10",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@ -100,7 +100,7 @@
"babel-plugin-tsconfig-paths": "^1.0.3",
"chokidar": "^3.6.0",
"cross-env": "^7.0.3",
"esbuild": "^0.20.2",
"esbuild": "^0.21.0",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
@ -117,7 +117,7 @@
"react-dom": "^18.3.1",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.0.9",
"storybook": "^8.0.10",
"storybook-addon-mock": "^5.0.0",
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
@ -126,9 +126,9 @@
"vite-tsconfig-paths": "^4.3.2"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.20.1",
"@esbuild/darwin-arm64": "^0.21.0",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.20.1",
"@esbuild/linux-arm64": "^0.21.0",
"@rollup/rollup-darwin-arm64": "4.17.2",
"@rollup/rollup-linux-arm64-gnu": "4.17.2",
"@rollup/rollup-linux-x64-gnu": "4.17.2"

View File

@ -97,7 +97,7 @@ export class EventListPage extends TablePage<Event> {
}
renderExpanded(item: Event): TemplateResult {
return html` <td role="cell" colspan="3">
return html` <td role="cell" colspan="5">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>

View File

@ -11,6 +11,7 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { map } from "lit/directives/map.js";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
@ -121,120 +122,35 @@ export class PromptForm extends ModelForm<Prompt, string> {
}
renderTypes(): TemplateResult {
return html`
<option
value=${PromptTypeEnum.Text}
?selected=${this.instance?.type === PromptTypeEnum.Text}
>
${msg("Text: Simple Text input")}
</option>
<option
value=${PromptTypeEnum.TextArea}
?selected=${this.instance?.type === PromptTypeEnum.TextArea}
>
${msg("Text Area: Multiline text input")}
</option>
<option
value=${PromptTypeEnum.TextReadOnly}
?selected=${this.instance?.type === PromptTypeEnum.TextReadOnly}
>
${msg("Text (read-only): Simple Text input, but cannot be edited.")}
</option>
<option
value=${PromptTypeEnum.TextAreaReadOnly}
?selected=${this.instance?.type === PromptTypeEnum.TextAreaReadOnly}
>
${msg("Text Area (read-only): Multiline text input, but cannot be edited.")}
</option>
<option
value=${PromptTypeEnum.Username}
?selected=${this.instance?.type === PromptTypeEnum.Username}
>
${msg(
"Username: Same as Text input, but checks for and prevents duplicate usernames.",
)}
</option>
<option
value=${PromptTypeEnum.Email}
?selected=${this.instance?.type === PromptTypeEnum.Email}
>
${msg("Email: Text field with Email type.")}
</option>
<option
value=${PromptTypeEnum.Password}
?selected=${this.instance?.type === PromptTypeEnum.Password}
>
${msg(
"Password: Masked input, multiple inputs of this type on the same prompt need to be identical.",
)}
</option>
<option
value=${PromptTypeEnum.Number}
?selected=${this.instance?.type === PromptTypeEnum.Number}
>
${msg("Number")}
</option>
<option
value=${PromptTypeEnum.Checkbox}
?selected=${this.instance?.type === PromptTypeEnum.Checkbox}
>
${msg("Checkbox")}
</option>
<option
value=${PromptTypeEnum.RadioButtonGroup}
?selected=${this.instance?.type === PromptTypeEnum.RadioButtonGroup}
>
${msg("Radio Button Group (fixed choice)")}
</option>
<option
value=${PromptTypeEnum.Dropdown}
?selected=${this.instance?.type === PromptTypeEnum.Dropdown}
>
${msg("Dropdown (fixed choice)")}
</option>
<option
value=${PromptTypeEnum.Date}
?selected=${this.instance?.type === PromptTypeEnum.Date}
>
${msg("Date")}
</option>
<option
value=${PromptTypeEnum.DateTime}
?selected=${this.instance?.type === PromptTypeEnum.DateTime}
>
${msg("Date Time")}
</option>
<option
value=${PromptTypeEnum.File}
?selected=${this.instance?.type === PromptTypeEnum.File}
>
${msg("File")}
</option>
<option
value=${PromptTypeEnum.Separator}
?selected=${this.instance?.type === PromptTypeEnum.Separator}
>
${msg("Separator: Static Separator Line")}
</option>
<option
value=${PromptTypeEnum.Hidden}
?selected=${this.instance?.type === PromptTypeEnum.Hidden}
>
${msg("Hidden: Hidden field, can be used to insert data into form.")}
</option>
<option
value=${PromptTypeEnum.Static}
?selected=${this.instance?.type === PromptTypeEnum.Static}
>
${msg("Static: Static value, displayed as-is.")}
</option>
<option
value=${PromptTypeEnum.AkLocale}
?selected=${this.instance?.type === PromptTypeEnum.AkLocale}
>
${msg("authentik: Locale: Displays a list of locales authentik supports.")}
</option>
`;
// prettier-ignore
const promptTypesWithLabels = [
[PromptTypeEnum.Text, msg("Text: Simple Text input")],
[PromptTypeEnum.TextArea, msg("Text Area: Multiline text input")],
[PromptTypeEnum.TextReadOnly, msg("Text (read-only): Simple Text input, but cannot be edited.")],
[PromptTypeEnum.TextAreaReadOnly, msg("Text Area (read-only): Multiline text input, but cannot be edited.")],
[PromptTypeEnum.Username, msg("Username: Same as Text input, but checks for and prevents duplicate usernames.")],
[PromptTypeEnum.Email, msg("Email: Text field with Email type.")],
[PromptTypeEnum.Password, msg("Password: Masked input, multiple inputs of this type on the same prompt need to be identical.")],
[PromptTypeEnum.Number, msg("Number")],
[PromptTypeEnum.Checkbox, msg("Checkbox")],
[PromptTypeEnum.RadioButtonGroup, msg("Radio Button Group (fixed choice)")],
[PromptTypeEnum.Dropdown, msg("Dropdown (fixed choice)")],
[PromptTypeEnum.Date, msg("Date")],
[PromptTypeEnum.DateTime, msg("Date Time")],
[PromptTypeEnum.File, msg("File")],
[PromptTypeEnum.Separator, msg("Separator: Static Separator Line")],
[PromptTypeEnum.Hidden, msg("Hidden: Hidden field, can be used to insert data into form.")],
[PromptTypeEnum.Static, msg("Static: Static value, displayed as-is.")],
[PromptTypeEnum.AkLocale, msg("authentik: Locale: Displays a list of locales authentik supports.")],
];
const currentType = this.instance?.type;
return html` ${map(
promptTypesWithLabels,
([promptType, label]) =>
html`<option value=${promptType} ?selected=${promptType === currentType}>
${label}
</option>`,
)}`;
}
renderForm(): TemplateResult {

View File

@ -90,6 +90,27 @@ export class SystemTaskListPage extends TablePage<SystemTask> {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Expiry")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${item.expiring
? html`
<pf-tooltip
position="top"
content=${(
item.expires || new Date()
).toLocaleString()}
>
${getRelativeTime(item.expires || new Date())}
</pf-tooltip>
`
: msg("-")}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Messages")}</span>

View File

@ -18,6 +18,7 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFSplit from "@patternfly/patternfly/layouts/Split/split.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventActions, FlowsApi } from "@goauthentik/api";
@ -81,6 +82,7 @@ export class EventInfo extends AKElement {
PFCard,
PFTable,
PFList,
PFSplit,
PFDescriptionList,
css`
code {
@ -246,11 +248,17 @@ export class EventInfo extends AKElement {
renderModelChanged() {
const diff = this.event.context.diff as unknown as {
[key: string]: { new_value: unknown; previous_value: unknown };
[key: string]: {
new_value: unknown;
previous_value: unknown;
add?: unknown[];
remove?: unknown[];
clear?: boolean;
};
};
let diffBody = html``;
if (diff) {
diffBody = html`<div class="pf-l-flex__item">
diffBody = html`<div class="pf-l-split__item pf-m-fill">
<div class="pf-c-card__title">${msg("Changes made:")}</div>
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<thead>
@ -262,16 +270,36 @@ export class EventInfo extends AKElement {
</thead>
<tbody role="rowgroup">
${Object.keys(diff).map((key) => {
const value = diff[key];
const previousCol = value.previous_value
? JSON.stringify(value.previous_value, null, 4)
: msg("-");
let newCol = html``;
if (value.add || value.remove) {
newCol = html`<ul class="pf-c-list">
${(value.add || value.remove)?.map((item) => {
let itemLabel = "";
if (value.add) {
itemLabel = msg(str`Added ID ${item}`);
} else if (value.remove) {
itemLabel = msg(str`Removed ID ${item}`);
}
return html`<li>${itemLabel}</li>`;
})}
</ul>`;
} else if (value.clear) {
newCol = html`${msg("Cleared")}`;
} else {
newCol = html`<pre>
${JSON.stringify(value.new_value, null, 4)}</pre
>`;
}
return html` <tr role="row">
<td role="cell"><pre>${key}</pre></td>
<td role="cell">
<pre>
${JSON.stringify(diff[key].previous_value, null, 4)}</pre
>
</td>
<td role="cell">
<pre>${JSON.stringify(diff[key].new_value, null, 4)}</pre>
<pre>${previousCol}</pre>
</td>
<td role="cell">${newCol}</td>
</tr>`;
})}
</tbody>
@ -280,8 +308,8 @@ ${JSON.stringify(diff[key].previous_value, null, 4)}</pre
</div>`;
}
return html`
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-l-split">
<div class="pf-l-split__item pf-m-fill">
<div class="pf-c-card__title">${msg("Affected model:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(this.event.context?.model as EventModel)}

View File

@ -6555,6 +6555,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -6824,6 +6824,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -6472,6 +6472,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -8599,27 +8599,47 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
<target>La requête a échoué. Veuillez réessayer plus tard. </target>
</trans-unit>
<trans-unit id="s85be1f5e7a0fa3b1">
<source>Available Roles</source>
<target>Rôles disponibles</target>
</trans-unit>
<trans-unit id="sa59d53ee922c08b5">
<source>Selected Roles</source>
<target>Rôles sélectionnés</target>
</trans-unit>
<trans-unit id="s7bfbf84a8ad5883f">
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
<target>Les comptes de service internes sont créés et gérés par authentik et ne peuvent pas être créés manuellement.</target>
</trans-unit>
<trans-unit id="sb0680e5f8782db1e">
<source>Private key Algorithm</source>
<target>Algorithme de clé privée</target>
</trans-unit>
<trans-unit id="sa03cbf19feebd249">
<source>RSA</source>
<target>RSA</target>
</trans-unit>
<trans-unit id="sfbfccbd3e395c147">
<source>ECDSA</source>
<target>ECDSA</target>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
<target>Algorithme utilisé pour générer la clé privée.</target>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
<target>ID <x id="0" equiv-text="${item}"/> ajouté</target>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
<target>ID <x id="0" equiv-text="${item}"/> supprimé</target>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
<target>Effacé</target>
</trans-unit>
</body>
</file>

View File

@ -8410,6 +8410,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -8254,6 +8254,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -6676,6 +6676,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -8526,4 +8526,13 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body></file></xliff>

View File

@ -6465,6 +6465,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -5383,6 +5383,15 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -8613,18 +8613,35 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s7bfbf84a8ad5883f">
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
<target>内部服务账户由 authentik 创建和管理,无法手动创建。</target>
</trans-unit>
<trans-unit id="sb0680e5f8782db1e">
<source>Private key Algorithm</source>
<target>私钥算法</target>
</trans-unit>
<trans-unit id="sa03cbf19feebd249">
<source>RSA</source>
<target>RSA</target>
</trans-unit>
<trans-unit id="sfbfccbd3e395c147">
<source>ECDSA</source>
<target>ECDSA</target>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
<target>用于生成私钥的算法。</target>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
<target>已添加 ID <x id="0" equiv-text="${item}"/></target>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
<target>已删除 ID <x id="0" equiv-text="${item}"/></target>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
<target>已清除</target>
</trans-unit>
</body>
</file>

View File

@ -6513,6 +6513,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

View File

@ -8610,6 +8610,38 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa59d53ee922c08b5">
<source>Selected Roles</source>
<target>已选角色</target>
</trans-unit>
<trans-unit id="s7bfbf84a8ad5883f">
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
<target>内部服务账户由 authentik 创建和管理,无法手动创建。</target>
</trans-unit>
<trans-unit id="sb0680e5f8782db1e">
<source>Private key Algorithm</source>
<target>私钥算法</target>
</trans-unit>
<trans-unit id="sa03cbf19feebd249">
<source>RSA</source>
<target>RSA</target>
</trans-unit>
<trans-unit id="sfbfccbd3e395c147">
<source>ECDSA</source>
<target>ECDSA</target>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
<target>用于生成私钥的算法。</target>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
<target>已添加 ID <x id="0" equiv-text="${item}"/></target>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
<target>已删除 ID <x id="0" equiv-text="${item}"/></target>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
<target>已清除</target>
</trans-unit>
</body>
</file>

View File

@ -8371,6 +8371,15 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sebea299e52db4242">
<source>Algorithm used to generate the private key.</source>
</trans-unit>
<trans-unit id="s21e38ec6ffa2b918">
<source>Added ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s74918a09ef39ed26">
<source>Removed ID <x id="0" equiv-text="${item}"/></source>
</trans-unit>
<trans-unit id="s3a20e5d7dd70dcd3">
<source>Cleared</source>
</trans-unit>
</body>
</file>

1
website/.gitignore vendored
View File

@ -22,3 +22,4 @@ yarn-error.log*
static/docker-compose.yml
static/schema.yaml
developer-docs/api/reference/**

View File

@ -1,9 +0,0 @@
---
hide_table_of_contents: true
---
# API Browser
import ApiDocMdx from "@theme/ApiDocMdx";
<ApiDocMdx id="main" />

View File

@ -60,7 +60,7 @@ Depending on the flow, you'll either get a 200 Response with another challenge,
Depending also on the stage, a response might take longer to be returned (especially with the Duo Authenticator validation).
To see the data layout for every stage possible, see the [API Browser](https://goauthentik.io/api/#get-/api/v3/flows/executor/-flow_slug-/)
To see the data layout for every stage possible, see the [API Browser](./reference/flows-executor-get)
## Result

View File

@ -16,7 +16,7 @@ The most common types are:
### Add a new integration
To add documentation for a new [integration](../../../integrations/) (with support level Community or Vendor), please use the integration template [`service.md`](https://github.com/goauthentik/authentik/blob/main/website/integrations/_template/service.md) file from our GitHub repo. You can download the template file using the following command:
To add documentation for a new integration (with support level Community or Vendor), please use the integration template [`service.md`](https://github.com/goauthentik/authentik/blob/main/website/integrations/_template/service.md) file from our GitHub repo. You can download the template file using the following command:
```shell
wget https://raw.githubusercontent.com/goauthentik/authentik/main/website/integrations/_template/service.md

View File

@ -58,7 +58,7 @@ Be sure to disable the embedded outpost with `AUTHENTIK_OUTPOSTS__DISABLE_EMBEDD
### 2. Create a new tenant with authentik API endpoints
Tenants are created using the API routes associated. Search for `tenant` in the [API browser](../../developer-docs/api/) for the available endpoints.
Tenants are created using the API routes associated. Search for `tenant` in the [API browser](/developer-docs/api/reference/authentik) for the available endpoints.
When creating a tenant you must specify a `name`, used for display purposes, and a `schema_name`, used to create the PostgreSQL schema associated with the tenant.

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

View File

@ -1,38 +1,65 @@
---
title: Inspector
title: Flow Inspector
---
The flow inspector, introduced in 2021.10, allows administrators to easily figure out how custom flows work, inspect the current context and debug issues.
The flow inspector, introduced in 2021.10, allows administrators to visually determine how custom flows work, inspect the current [flow context](../flow/context/index.md), and investigate issues.
As shown in the screenshot below, the flow inspector displays next to the selected flow (in this case, "Change Password"), with [information](#flow-inspector-details) about that specific flow and flow context.
![](./flow-inspector.png)
## Access the Flow Inspector
:::info
When running a flow with the inspector enabled, the flow is still executed normally. This means that for example, a [User write](../flow/stages/user_write.md) stage will _actually_ write user data.
Be aware that when running a flow with the inspector enabled, the flow is still executed normally. This means that for example, a [User write](../flow/stages/user_write.md) stage _will_ write user data.
:::
### Accessing the inspector
### Permissions and debug mode
By default, the inspector can only be enabled when the currently authenticated user is a superuser.
When developing authentik with the debug-mode enabled, the inspector is enabled by default and can be accessed by both unauthenticated users and standard users. However the debug-mode should only be used for the development of authentik.
By default, the inspector is only enabled when the currently authenticated user is a superuser, OR if a user has been granted the [permission](../user-group-role/access-control/permissions.md) **Can inspect a Flow's execution** (or is a user assigned to role with the permission).
![](./inspector.png)
When developing authentik with the debug mode enabled, the inspector is enabled by default and can be accessed by both unauthenticated users and standard users. However the debug mode should only be used for the development of authentik. So unless you are a developer and need the more verbose error information, the best practice for using the flow inspector is to assign the permission, not use debug mode.
The following infos are shown in the inspector
### Open the Flow Inspector
## Next stage
1. To access the inspector, open the Admin interface and navigate to **Flows and Stages -> Flows**.
This is the currently planned next stage. If you have stage bindings configured to _Evaluate when flow is planned_, then you will see the result here. If you however have them configured to re-evaluate (_Evaluate when stage is run_), then this will not show up here, since the results will vary based on your input.
2. Select the specific flow that you want to inspect by clicking its name in the list.
3. On the Flow's detail page, on the left side under **Execute Flow**, click **with inspector**.
4. The selected flow will launch in a new browser tab, with the flow inspector displayed to the right.
Alternatively, a user with the correct permission can launch the inspector by adding the query parameter `?inspector` to the URL when the URL opens on a flow.
:::info
Troubleshooting:
- If the flow inspector does not launch and a "Bad request" error displays, this is likely either because you selected a flow that is not defined in your instance or the flow has a policy bound directly to it that prevents access, so the inspector won't open because the flow can't run results.
- If the flow inspector launches but is empty, you can refresh the browser or advance the flow to load the inspector. This can occur when a race condition happens (the inspector tries to fetch the data before the flow plan is fully planned and as such the panel just shows blank).
:::
### Flow Inspector Details
The following information is shown in the inspector:
#### Next stage
This is the currently planned next stage. If you have stage bindings configured to `Evaluate when flow is planned`\_`, then you will see the result here. If, however, you have them configured to re-evaluate (`Evaluate when stage is run`), then this will not show up here, since the results will vary based on your input.
Shown is the name and kind of the stage, as well as the unique ID.
## Plan history
#### Plan history
Here you can see an overview of which stages have run, which is currently active, and which is planned to come next. Same caveats as above apply.
## Current plan context
#### Current plan context
This shows you the current context. This will contain fields depending on the same, after an identification stage for example you would see "pending_user" be set.
This shows you the current context. This will contain fields depending on the same, after an identification stage for example you would see "pending_user" defined.
This data is not cleaned, so if your flow involves inputting a password, it will be shown here too.
## Session ID
#### Session ID
The unique ID for the currently used session. This can be used to debug issues with flows restarting/losing state.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -12,7 +12,7 @@ Select which fields the user can use to identify themselves. Multiple fields can
- Email
- UPN
UPN will attempt to identify the user based on the `upn` attribute, which can be imported with an [LDAP Source](/integrations/sources/ldap/)
UPN will attempt to identify the user based on the `upn` attribute, which can be imported with an [LDAP Source](/docs/sources/ldap/)
## Password stage
@ -32,7 +32,7 @@ When enabled, any user identifier will be accepted as valid (as long as they mat
## Source settings
Some sources (like the [OAuth Source](../../../../integrations/sources/oauth/) and [SAML Source](../../../../integrations/sources/saml/)) require user interaction. To make these sources available to users, they can be selected in the Identification stage settings, which will show them below the selected [user field](#user-fields).
Some sources (like the [OAuth Source](../../../../docs/sources/oauth/) and [SAML Source](../../../../docs/sources/saml/)) require user interaction. To make these sources available to users, they can be selected in the Identification stage settings, which will show them below the selected [user field](#user-fields).
By default, sources are only shown with their icon, which can be changed with the _Show sources' labels_ option.

View File

@ -7,7 +7,7 @@ title: Source stage
---
The source stage injects an [OAuth](../../../../integrations/sources/oauth/) or [SAML](../../../../integrations/sources/saml/) Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).
The source stage injects an [OAuth](../../../../docs/sources/oauth/) or [SAML](../../../../docs/sources/saml/) Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).
```mermaid
sequenceDiagram
@ -44,13 +44,13 @@ This stage can be used to leverage an external OAuth/SAML identity provider.
For example, you can authenticate users by routing them through a custom device-health solution.
Another use case is to route users to authenticate with your legacy (Okta, etc) IdP and then use the returned identity and attributes within authentik as part of an authorization flow, for example as part of an IdP migration. For authentication/enrollment this is also possible with an [OAuth](../../../../integrations/sources/oauth/)/[SAML](../../../../integrations/sources/saml/) source by itself.
Another use case is to route users to authenticate with your legacy (Okta, etc) IdP and then use the returned identity and attributes within authentik as part of an authorization flow, for example as part of an IdP migration. For authentication/enrollment this is also possible with an [OAuth](../../../../docs/sources/oauth/)/[SAML](../../../../docs/sources/saml/) source by itself.
### Options
#### Source
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../integrations/sources/oauth/) or [SAML](../../../../integrations/sources/saml/). Sources like [LDAP](../../../../integrations/sources/ldap/) are _not_ compatible.
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../docs/sources/oauth/) or [SAML](../../../../docs/sources/saml/). Sources like [LDAP](../../../../docs/sources/ldap/) are _not_ compatible.
#### Resume timeout

View File

@ -3,11 +3,11 @@ title: Welcome to authentik
slug: /
---
## About authentik technical documentation
## About authentik documentation
Our tech docs cover the typical topics, from installation to configuration, adding providers, defining policies and creating login flows, event monitoring, security, and attributes. [Enterprise](./enterprise/index.md) version documentation is included here, within our standard tech docs.
- For information about integrating a specific application or software into authentik, refer to our [Integrations](../integrations) section, accessible from the top menu bar.
- For information about integrating a specific application or software into authentik, refer to our Integrations section, accessible from the top menu bar.
- For developer-focused documentation, such as using our APIs and blueprints, setting up your development environment, translations, or how to contribute, refer to the [Developer](../developer-docs) area, accessible from the top menu bar.

View File

@ -50,3 +50,23 @@ services:
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: token-generated-by-authentik
```
### RADIUS outpost
```yaml
version: "3.5"
services:
radius_outpost:
image: ghcr.io/goauthentik/radius
# Optionally specify which networks the container should be
# might be needed to reach the core authentik server
# networks:
# - foo
ports:
- 1812:1812/udp
environment:
AUTHENTIK_HOST: https://your-authentik.tld
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: token-generated-by-authentik
```

View File

@ -7,7 +7,7 @@ slug: "/releases/2022.5"
- Twitter Source has been migrated to OAuth2
This requires some reconfiguration on both Twitter's and authentik's side. Check out the new Twitter integration docs [here](../../integrations/sources/twitter/)
This requires some reconfiguration on both Twitter's and authentik's side. Check out the new Twitter integration docs [here](../../docs/sources/twitter/)
- OAuth Provider: Redirect URIs are now checked using regular expressions

View File

@ -37,7 +37,7 @@ slug: /releases/2024.4
Provision users and groups in authentik using an SCIM API.
For details refer to [SCIM Source](../../../integrations/sources/scim/)
For details refer to [SCIM Source](../../../docs/sources/scim/)
- **Configurable WebAuthn device restrictions**
@ -57,7 +57,7 @@ slug: /releases/2024.4
When authentik is configured to federate with an LDAP source, upon authentication, authentik hashed the password and stored it in its own database. This allows authentication to function when LDAP is unreachable. Admins can now configure this behavior for when this is not desirable.
For details refer to [LDAP Source](../../../integrations/sources/ldap/)
For details refer to [LDAP Source](../../../docs/sources/ldap/)
- **Configurable app password token expiring**
@ -216,6 +216,25 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.4
- web: Add resolved and integrity fields back to package-lock.json (cherry-pick #9419) (#9421)
- web: markdown: display markdown even when frontmatter is missing (cherry-pick #9404) (#9405)
## Fixed in 2024.4.2
- core: fix source_flow_manager saving user-source connection too early (cherry-pick #9559) (#9578)
- core: fix task clean_expiring_models removing valid sessions with using database storage (cherry-pick #9598) (#9601)
- core: only prefetch related objects when required (cherry-pick #9476) (#9510)
- enterprise/audit: fix audit logging with m2m relations (cherry-pick #9571) (#9572)
- events: ensure all models' `__str__` can be called without any further lookups (cherry-pick #9480) (#9485)
- flows: fix execute API endpoint (cherry-pick #9478) (#9481)
- lifecycle: close database connection after migrating (cherry-pick #9516) (#9531)
- providers/saml: fix ECDSA support (cherry-pick #9537) (#9544)
- providers/scim: fix SCIM ID incorrectly used as primary key (cherry-pick #9557) (#9579)
- providers/scim: fix Sync task's time_limit not set correctly (cherry-pick #9546) (#9553)
- sources/oauth: fix OAuth2 Client sending token request incorrectly (cherry-pick #9474) (#9475)
- sources/scim: fix duplicate groups and invalid schema (cherry-pick #9466) (#9606)
- tenants: fix scheduled tasks not running on default tenant (cherry-pick #9583) (#9586)
- web/flows: fix error when enrolling multiple WebAuthn devices consecutively (cherry-pick #9545) (#9547)
- web/flows: fix missing fallback for flow logo (cherry-pick #9487) (#9492)
- web: Add missing integrity hashes to package-lock.json (#9527)
## API Changes
#### What's New

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

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