diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 4f7cf5256..282543d96 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -61,10 +61,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup authentik env - uses: ./.github/actions/setup - with: - postgresql_version: ${{ matrix.psql }} - name: checkout stable run: | # Delete all poetry envs @@ -76,7 +72,7 @@ jobs: git checkout version/$(python -c "from authentik import __version__; print(__version__)") rm -rf .github/ scripts/ mv ../.github ../scripts . - - name: Setup authentik env (ensure stable deps are installed) + - name: Setup authentik env (stable) uses: ./.github/actions/setup with: postgresql_version: ${{ matrix.psql }} @@ -90,15 +86,20 @@ jobs: git clean -d -fx . git checkout $GITHUB_SHA # Delete previous poetry env - rm -rf $(poetry env info --path) + rm -rf /home/runner/.cache/pypoetry/virtualenvs/* - name: Setup authentik env (ensure latest deps are installed) uses: ./.github/actions/setup with: postgresql_version: ${{ matrix.psql }} - name: migrate to latest run: | - poetry install poetry run python -m lifecycle.migrate + - name: run tests + env: + # Test in the main database that we just migrated from the previous stable version + AUTHENTIK_POSTGRESQL__TEST__NAME: authentik + run: | + poetry run make test test-unittest: name: test-unittest - PostgreSQL ${{ matrix.psql }} runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index c649ff230..a91ca7464 100644 --- a/Makefile +++ b/Makefile @@ -115,8 +115,9 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a npx prettier --write diff.md gen-clean: - rm -rf web/api/src/ - rm -rf api/ + rm -rf gen-go-api/ + rm -rf gen-ts-api/ + rm -rf web/node_modules/@goauthentik/api/ gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application docker run \ diff --git a/authentik/api/tests/test_auth.py b/authentik/api/tests/test_auth.py index cd23a1835..c09bca5a3 100644 --- a/authentik/api/tests/test_auth.py +++ b/authentik/api/tests/test_auth.py @@ -12,6 +12,8 @@ from authentik.blueprints.tests import reconcile_app from authentik.core.models import Token, TokenIntents, User, UserTypes from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.lib.generators import generate_id +from authentik.outposts.apps import MANAGED_OUTPOST +from authentik.outposts.models import Outpost from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API from authentik.providers.oauth2.models import AccessToken, OAuth2Provider @@ -49,8 +51,12 @@ class TestAPIAuth(TestCase): with self.assertRaises(AuthenticationFailed): bearer_auth(f"Bearer {token.key}".encode()) - def test_managed_outpost(self): + @reconcile_app("authentik_outposts") + def test_managed_outpost_fail(self): """Test managed outpost""" + outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first() + outpost.user.delete() + outpost.delete() with self.assertRaises(AuthenticationFailed): bearer_auth(f"Bearer {settings.SECRET_KEY}".encode()) diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index 721eb5dcb..7abf488da 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema, inline_serializer from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.fields import CharField, DateTimeField, JSONField +from rest_framework.fields import CharField, DateTimeField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ListSerializer, ModelSerializer @@ -15,7 +15,7 @@ from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.core.api.used_by import UsedByMixin -from authentik.core.api.utils import PassiveSerializer +from authentik.core.api.utils import JSONDictField, PassiveSerializer class ManagedSerializer: @@ -28,7 +28,7 @@ class MetadataSerializer(PassiveSerializer): """Serializer for blueprint metadata""" name = CharField() - labels = JSONField() + labels = JSONDictField() class BlueprintInstanceSerializer(ModelSerializer): diff --git a/authentik/blueprints/v1/meta/apply_blueprint.py b/authentik/blueprints/v1/meta/apply_blueprint.py index 5946342a3..0a8d84e66 100644 --- a/authentik/blueprints/v1/meta/apply_blueprint.py +++ b/authentik/blueprints/v1/meta/apply_blueprint.py @@ -2,11 +2,11 @@ from typing import TYPE_CHECKING from rest_framework.exceptions import ValidationError -from rest_framework.fields import BooleanField, JSONField +from rest_framework.fields import BooleanField from structlog.stdlib import get_logger from authentik.blueprints.v1.meta.registry import BaseMetaModel, MetaResult, registry -from authentik.core.api.utils import PassiveSerializer, is_dict +from authentik.core.api.utils import JSONDictField, PassiveSerializer if TYPE_CHECKING: from authentik.blueprints.models import BlueprintInstance @@ -17,7 +17,7 @@ LOGGER = get_logger() class ApplyBlueprintMetaSerializer(PassiveSerializer): """Serializer for meta apply blueprint model""" - identifiers = JSONField(validators=[is_dict]) + identifiers = JSONDictField() required = BooleanField(default=True) # We cannot override `instance` as that will confuse rest_framework diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index 21ba19974..04670844d 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -8,7 +8,7 @@ from django_filters.filterset import FilterSet from drf_spectacular.utils import OpenApiResponse, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import CharField, IntegerField, JSONField +from rest_framework.fields import CharField, IntegerField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError @@ -16,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.decorators import permission_required from authentik.core.api.used_by import UsedByMixin -from authentik.core.api.utils import PassiveSerializer, is_dict +from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.models import Group, User from authentik.rbac.api.roles import RoleSerializer @@ -24,7 +24,7 @@ from authentik.rbac.api.roles import RoleSerializer class GroupMemberSerializer(ModelSerializer): """Stripped down user serializer to show relevant users for groups""" - attributes = JSONField(validators=[is_dict], required=False) + attributes = JSONDictField(required=False) uid = CharField(read_only=True) class Meta: @@ -44,7 +44,7 @@ class GroupMemberSerializer(ModelSerializer): class GroupSerializer(ModelSerializer): """Group Serializer""" - attributes = JSONField(validators=[is_dict], required=False) + attributes = JSONDictField(required=False) users_obj = ListSerializer( child=GroupMemberSerializer(), read_only=True, source="users", required=False ) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index f8cb5d18f..45345f8d0 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -32,13 +32,7 @@ from drf_spectacular.utils import ( ) from guardian.shortcuts import get_anonymous_user, get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import ( - CharField, - IntegerField, - JSONField, - ListField, - SerializerMethodField, -) +from rest_framework.fields import CharField, IntegerField, ListField, SerializerMethodField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( @@ -58,7 +52,7 @@ from authentik.api.decorators import permission_required from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.brands.models import Brand from authentik.core.api.used_by import UsedByMixin -from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict +from authentik.core.api.utils import JSONDictField, LinkSerializer, PassiveSerializer from authentik.core.middleware import ( SESSION_KEY_IMPERSONATE_ORIGINAL_USER, SESSION_KEY_IMPERSONATE_USER, @@ -88,7 +82,7 @@ LOGGER = get_logger() class UserGroupSerializer(ModelSerializer): """Simplified Group Serializer for user's groups""" - attributes = JSONField(required=False) + attributes = JSONDictField(required=False) parent_name = CharField(source="parent.name", read_only=True) class Meta: @@ -109,7 +103,7 @@ class UserSerializer(ModelSerializer): is_superuser = BooleanField(read_only=True) avatar = CharField(read_only=True) - attributes = JSONField(validators=[is_dict], required=False) + attributes = JSONDictField(required=False) groups = PrimaryKeyRelatedField( allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all(), default=list ) diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index cf1870197..c7a188f5c 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -2,6 +2,9 @@ from typing import Any from django.db.models import Model +from drf_spectacular.extensions import OpenApiSerializerFieldExtension +from drf_spectacular.plumbing import build_basic_type +from drf_spectacular.types import OpenApiTypes from rest_framework.fields import CharField, IntegerField, JSONField from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError @@ -13,6 +16,21 @@ def is_dict(value: Any): raise ValidationError("Value must be a dictionary, and not have any duplicate keys.") +class JSONDictField(JSONField): + """JSON Field which only allows dictionaries""" + + default_validators = [is_dict] + + +class JSONExtension(OpenApiSerializerFieldExtension): + """Generate API Schema for JSON fields as""" + + target_class = "authentik.core.api.utils.JSONDictField" + + def map_serializer_field(self, auto_schema, direction): + return build_basic_type(OpenApiTypes.OBJECT) + + class PassiveSerializer(Serializer): """Base serializer class which doesn't implement create/update methods""" @@ -26,7 +44,7 @@ class PassiveSerializer(Serializer): class PropertyMappingPreviewSerializer(PassiveSerializer): """Preview how the current user is mapped via the property mappings selected in a provider""" - preview = JSONField(read_only=True) + preview = JSONDictField(read_only=True) class MetaNameSerializer(PassiveSerializer): diff --git a/authentik/core/models.py b/authentik/core/models.py index 75706c20e..2352055cc 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -30,7 +30,6 @@ from authentik.lib.models import ( DomainlessFormattedURLValidator, SerializerModel, ) -from authentik.lib.utils.http import get_client_ip from authentik.policies.models import PolicyBindingModel from authentik.root.install_id import get_install_id @@ -748,12 +747,14 @@ class AuthenticatedSession(ExpiringModel): @staticmethod def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]: """Create a new session from a http request""" + from authentik.root.middleware import ClientIPMiddleware + if not hasattr(request, "session") or not request.session.session_key: return None return AuthenticatedSession( session_key=request.session.session_key, user=user, - last_ip=get_client_ip(request), + last_ip=ClientIPMiddleware.get_client_ip(request), last_user_agent=request.META.get("HTTP_USER_AGENT", ""), expires=request.session.get_expiry_date(), ) diff --git a/authentik/core/templates/login/base_full.html b/authentik/core/templates/login/base_full.html index 4bc5868f2..c88692a63 100644 --- a/authentik/core/templates/login/base_full.html +++ b/authentik/core/templates/login/base_full.html @@ -44,28 +44,14 @@ {% block body %}