Skip to content

Commit

Permalink
Merge branch 'main' into web/sidebar-with-live-content-3
Browse files Browse the repository at this point in the history
* main: (42 commits)
  stages/authenticator_totp: fix API validation error due to choices (#7608)
  website: fix pricing page inconsistency (#7607)
  web: bump API Client version (#7602)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7603)
  core: bump goauthentik.io/api/v3 from 3.2023103.2 to 3.2023103.3 (#7606)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7604)
  Revert "web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web (#7486)"
  root: fix API schema for kotlin (#7601)
  web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web (#7486)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7583)
  events: fix missing model_* events when not directly authenticated (#7588)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7594)
  providers/scim: fix missing schemas attribute for User and Group (#7477)
  core: bump pydantic from 2.5.0 to 2.5.1 (#7592)
  web/admin: contextually add user to group when creating user from group page (#7586)
  website/blog: title and slug change (#7585)
  events: sanitize functions (#7587)
  stages/email: use uuid for email confirmation token instead of username (#7581)
  website/blog: Blog about zero trust and wireguard (#7567)
  ci: translation-advice: avoid commenting after make i18n-extract
  ...
  • Loading branch information
kensternberg-authentik committed Nov 17, 2023
2 parents 25ecc21 + ce86b20 commit d5875a5
Show file tree
Hide file tree
Showing 53 changed files with 1,469 additions and 917 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Extract version number
id: get_version
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/translation-advice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ on:
paths:
- "!**"
- "locale/**"
- "web/src/locales/**"
- "!locale/en/**"
- "web/xliff/**"

jobs:
post-comment:
Expand Down
12 changes: 6 additions & 6 deletions authentik/admin/api/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RuntimeDict(TypedDict):
uname: str


class SystemSerializer(PassiveSerializer):
class SystemInfoSerializer(PassiveSerializer):
"""Get system information."""

http_headers = SerializerMethodField()
Expand Down Expand Up @@ -91,14 +91,14 @@ class SystemView(APIView):
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
pagination_class = None
filter_backends = []
serializer_class = SystemSerializer
serializer_class = SystemInfoSerializer

@extend_schema(responses={200: SystemSerializer(many=False)})
@extend_schema(responses={200: SystemInfoSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Get system information."""
return Response(SystemSerializer(request).data)
return Response(SystemInfoSerializer(request).data)

@extend_schema(responses={200: SystemSerializer(many=False)})
@extend_schema(responses={200: SystemInfoSerializer(many=False)})
def post(self, request: Request) -> Response:
"""Get system information."""
return Response(SystemSerializer(request).data)
return Response(SystemInfoSerializer(request).data)
23 changes: 16 additions & 7 deletions authentik/events/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,30 @@ class AuditMiddleware:
of models"""

get_response: Callable[[HttpRequest], HttpResponse]
anonymous_user: User = None

def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response

def _ensure_fallback_user(self):
"""Defer fetching anonymous user until we have to"""
if self.anonymous_user:
return
from guardian.shortcuts import get_anonymous_user

self.anonymous_user = get_anonymous_user()

def connect(self, request: HttpRequest):
"""Connect signal for automatic logging"""
if not hasattr(request, "user"):
return
if not getattr(request.user, "is_authenticated", False):
return
self._ensure_fallback_user()
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
user = self.anonymous_user
if not hasattr(request, "request_id"):
return
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
post_save_handler = partial(self.post_save_handler, user=user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
post_save.connect(
post_save_handler,
dispatch_uid=request.request_id,
Expand Down
6 changes: 6 additions & 0 deletions authentik/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ def sanitize_item(value: Any) -> Any:
return value.isoformat()
if isinstance(value, timedelta):
return str(value.total_seconds())
if callable(value):
return {
"type": "callable",
"name": value.__name__,
"module": value.__module__,
}
return value


Expand Down
4 changes: 3 additions & 1 deletion authentik/providers/scim/clients/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def delete(self, obj: Group):

def to_scim(self, obj: Group) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = {}
raw_scim_group = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
}
for mapping in (
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
):
Expand Down
2 changes: 2 additions & 0 deletions authentik/providers/scim/clients/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
class User(BaseUser):
"""Modified User schema with added externalId field"""

schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
externalId: Optional[str] = None


class Group(BaseGroup):
"""Modified Group schema with added externalId field"""

schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
externalId: Optional[str] = None


Expand Down
4 changes: 3 additions & 1 deletion authentik/providers/scim/clients/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def delete(self, obj: User):

def to_scim(self, obj: User) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = {}
raw_scim_user = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
}
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
if not isinstance(mapping, SCIMMapping):
continue
Expand Down
18 changes: 15 additions & 3 deletions authentik/providers/scim/tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ def test_group_create(self, mock: Mocker):
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)

@Mocker()
Expand Down Expand Up @@ -96,7 +100,11 @@ def test_group_create_update(self, mock: Mocker):
validate(body, loads(schema.read()))
self.assertEqual(
body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
group.save()
self.assertEqual(mock.call_count, 4)
Expand Down Expand Up @@ -129,7 +137,11 @@ def test_group_create_delete(self, mock: Mocker):
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
group.delete()
self.assertEqual(mock.call_count, 4)
Expand Down
20 changes: 15 additions & 5 deletions authentik/providers/scim/tests/test_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def test_member_add(self):
self.assertJSONEqual(
mocker.request_history[3].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"emails": [],
"active": True,
"externalId": user.uid,
Expand All @@ -99,7 +100,11 @@ def test_member_add(self):
)
self.assertJSONEqual(
mocker.request_history[5].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)

with Mocker() as mocker:
Expand All @@ -118,14 +123,14 @@ def test_member_add(self):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)

Expand Down Expand Up @@ -174,6 +179,7 @@ def test_member_remove(self):
self.assertJSONEqual(
mocker.request_history[3].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"displayName": "",
"emails": [],
Expand All @@ -184,7 +190,11 @@ def test_member_remove(self):
)
self.assertJSONEqual(
mocker.request_history[5].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)

with Mocker() as mocker:
Expand All @@ -203,14 +213,14 @@ def test_member_remove(self):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)

Expand All @@ -230,13 +240,13 @@ def test_member_remove(self):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members",
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)
4 changes: 4 additions & 0 deletions authentik/providers/scim/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def test_user_create(self, mock: Mocker):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
Expand Down Expand Up @@ -121,6 +122,7 @@ def test_user_create_update(self, mock: Mocker):
self.assertEqual(
body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
Expand Down Expand Up @@ -173,6 +175,7 @@ def test_user_create_delete(self, mock: Mocker):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
Expand Down Expand Up @@ -240,6 +243,7 @@ def test_sync_task(self, mock: Mocker):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
Expand Down
9 changes: 8 additions & 1 deletion authentik/stages/authenticator_totp/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""AuthenticatorTOTPStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.fields import ChoiceField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
Expand All @@ -9,12 +10,18 @@
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDevice
from authentik.stages.authenticator_totp.models import (
AuthenticatorTOTPStage,
TOTPDevice,
TOTPDigits,
)


class AuthenticatorTOTPStageSerializer(StageSerializer):
"""AuthenticatorTOTPStage Serializer"""

digits = ChoiceField(choices=TOTPDigits.choices)

class Meta:
model = AuthenticatorTOTPStage
fields = StageSerializer.Meta.fields + ["configure_flow", "friendly_name", "digits"]
Expand Down
2 changes: 1 addition & 1 deletion authentik/stages/authenticator_totp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from authentik.stages.authenticator.util import hex_validator, random_hex


class TOTPDigits(models.IntegerChoices):
class TOTPDigits(models.TextChoices):
"""OTP Time Digits"""

SIX = 6, _("6 digits, widely compatible")
Expand Down
3 changes: 2 additions & 1 deletion authentik/stages/email/stage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""authentik multi-stage authentication engine"""
from datetime import timedelta
from uuid import uuid4

from django.contrib import messages
from django.http import HttpRequest, HttpResponse
Expand Down Expand Up @@ -71,7 +72,7 @@ def get_token(self) -> FlowToken:
valid_delta = timedelta(
minutes=current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down
identifier = slugify(f"ak-email-stage-{current_stage.name}-{pending_user}")
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
if not tokens.exists():
Expand Down
31 changes: 27 additions & 4 deletions authentik/stages/user_write/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
Expand Down Expand Up @@ -58,11 +59,33 @@ def test_user_create(self):
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
self.assertTrue(user_qs.exists())
self.assertTrue(user_qs.first().check_password(password))
self.assertEqual(
list(user_qs.first().ak_groups.order_by("name")), [self.other_group, self.group]
user = user_qs.first()
self.assertTrue(user.check_password(password))
self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})

self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model={
"app": "authentik_core",
"model_name": "user",
"pk": user.pk,
"name": "name",
},
)
)
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model={
"app": "authentik_core",
"model_name": "user",
"pk": user.pk,
"name": "name",
},
)
)
self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})

def test_user_update(self):
"""Test update of existing user"""
Expand Down

0 comments on commit d5875a5

Please sign in to comment.