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) ...
@ -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
|
||||
|
||||
2
.github/workflows/ci-outpost.yml
vendored
@ -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
|
||||
|
||||
1
Makefile
@ -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 \
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]}},
|
||||
)
|
||||
|
||||
@ -60,6 +60,8 @@ class SystemTaskSerializer(ModelSerializer):
|
||||
"duration",
|
||||
"status",
|
||||
"messages",
|
||||
"expires",
|
||||
"expiring",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@ -53,6 +53,7 @@ cache:
|
||||
|
||||
# result_backend:
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
|
||||
debug: false
|
||||
remote_debug: false
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")},
|
||||
),
|
||||
]
|
||||
@ -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}"
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 == "":
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
@ -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
@ -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=
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
95
internal/outpost/proxyv2/application/oauth_state.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
@ -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)"]
|
||||
|
||||
@ -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
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -22,3 +22,4 @@ yarn-error.log*
|
||||
|
||||
static/docker-compose.yml
|
||||
static/schema.yaml
|
||||
developer-docs/api/reference/**
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
# API Browser
|
||||
|
||||
import ApiDocMdx from "@theme/ApiDocMdx";
|
||||
|
||||
<ApiDocMdx id="main" />
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
BIN
website/docs/flow/flow-inspector.png
Normal file
|
After Width: | Height: | Size: 564 KiB |
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
|
||||

|
||||
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.
|
||||
|
||||
|
Before Width: | Height: | Size: 66 KiB |
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |