diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f78e67f49..d679517c7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2023.10.4 +current_version = 2023.10.5 tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/.dockerignore b/.dockerignore index 352faf761..8d20d66d6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,4 @@ blueprints/local .git !gen-ts-api/node_modules !gen-ts-api/dist/** +!gen-go-api/ diff --git a/.github/codespell-words.txt b/.github/codespell-words.txt index 71f2f1c2c..29fb24832 100644 --- a/.github/codespell-words.txt +++ b/.github/codespell-words.txt @@ -2,3 +2,4 @@ keypair keypairs hass warmup +ontext diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 26baa3556..71bfc0d7a 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,14 +86,20 @@ jobs: git clean -d -fx . git checkout $GITHUB_SHA # Delete previous poetry env - rm -rf $(poetry env info --path) - poetry install + 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 run python -m lifecycle.migrate + run: | + 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 @@ -247,12 +249,6 @@ jobs: VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }} cache-from: type=gha cache-to: type=gha,mode=max - - name: Comment on PR - if: github.event_name == 'pull_request' - continue-on-error: true - uses: ./.github/actions/comment-pr-instructions - with: - tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }} build-arm64: needs: ci-core-mark runs-on: ubuntu-latest @@ -301,3 +297,26 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max + pr-comment: + needs: + - build + - build-arm64 + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request' }} + permissions: + # Needed to write comments on PRs + pull-requests: write + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: prepare variables + uses: ./.github/actions/docker-push-variables + id: ev + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + - name: Comment on PR + uses: ./.github/actions/comment-pr-instructions + with: + tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }} diff --git a/.github/workflows/ci-outpost.yml b/.github/workflows/ci-outpost.yml index 196fa0b3b..35c83ac86 100644 --- a/.github/workflows/ci-outpost.yml +++ b/.github/workflows/ci-outpost.yml @@ -65,6 +65,7 @@ jobs: - proxy - ldap - radius + - rac runs-on: ubuntu-latest permissions: # Needed to upload contianer images to ghcr.io @@ -119,6 +120,7 @@ jobs: - proxy - ldap - radius + - rac goos: [linux] goarch: [amd64, arm64] steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5f1255f56..c8c0cc11f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,10 +27,10 @@ jobs: - name: Setup authentik env uses: ./.github/actions/setup - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c3c6a0d48..c002ab8a5 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -65,6 +65,7 @@ jobs: - proxy - ldap - radius + - rac steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 diff --git a/Dockerfile b/Dockerfile index 629d3258b..114be6253 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ # Stage 4: MaxMind GeoIP FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip -ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City" +ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN" ENV GEOIPUPDATE_VERBOSE="true" ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID" ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY" diff --git a/Makefile b/Makefile index c649ff230..93092a779 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ test: ## Run the server tests and produce a coverage report (locally) lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors. isort $(PY_SOURCES) black $(PY_SOURCES) - ruff $(PY_SOURCES) + ruff --fix $(PY_SOURCES) codespell -w $(CODESPELL_ARGS) lint: ## Lint the python and golang sources @@ -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/SECURITY.md b/SECURITY.md index 0d9d6a673..9bb674f23 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,9 @@ authentik takes security very seriously. We follow the rules of [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we urge our community to do so as well, instead of reporting vulnerabilities publicly. This allows us to patch the issue quickly, announce it's existence and release the fixed version. +## Independent audits and pentests + +In May/June of 2023 [Cure53](https://cure53.de) conducted an audit and pentest. The [results](https://cure53.de/pentest-report_authentik.pdf) are published on the [Cure53 website](https://cure53.de/#publications-2023). For more details about authentik's response to the findings of the audit refer to [2023-06 Cure53 Code audit](https://goauthentik.io/docs/security/2023-06-cure53). + ## What authentik classifies as a CVE CVE (Common Vulnerability and Exposure) is a system designed to aggregate all vulnerabilities. As such, a CVE will be issued when there is a either vulnerability or exposure. Per NIST, A vulnerability is: diff --git a/authentik/__init__.py b/authentik/__init__.py index fc368ceeb..0c651c834 100644 --- a/authentik/__init__.py +++ b/authentik/__init__.py @@ -2,7 +2,7 @@ from os import environ from typing import Optional -__version__ = "2023.10.4" +__version__ = "2023.10.5" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" 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/api/v3/config.py b/authentik/api/v3/config.py index 0defd1a5b..93b783629 100644 --- a/authentik/api/v3/config.py +++ b/authentik/api/v3/config.py @@ -19,7 +19,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from authentik.core.api.utils import PassiveSerializer -from authentik.events.geo import GEOIP_READER +from authentik.events.context_processors.base import get_context_processors from authentik.lib.config import CONFIG capabilities = Signal() @@ -30,6 +30,7 @@ class Capabilities(models.TextChoices): CAN_SAVE_MEDIA = "can_save_media" CAN_GEO_IP = "can_geo_ip" + CAN_ASN = "can_asn" CAN_IMPERSONATE = "can_impersonate" CAN_DEBUG = "can_debug" IS_ENTERPRISE = "is_enterprise" @@ -68,8 +69,9 @@ class ConfigView(APIView): deb_test = settings.DEBUG or settings.TEST if Path(settings.MEDIA_ROOT).is_mount() or deb_test: caps.append(Capabilities.CAN_SAVE_MEDIA) - if GEOIP_READER.enabled: - caps.append(Capabilities.CAN_GEO_IP) + for processor in get_context_processors(): + if cap := processor.capability(): + caps.append(cap) if CONFIG.get_bool("impersonation"): caps.append(Capabilities.CAN_IMPERSONATE) if settings.DEBUG: # pragma: no cover 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/apps.py b/authentik/blueprints/apps.py index 90df91c00..aba14d552 100644 --- a/authentik/blueprints/apps.py +++ b/authentik/blueprints/apps.py @@ -40,7 +40,7 @@ class ManagedAppConfig(AppConfig): meth() self._logger.debug("Successfully reconciled", name=name) except (DatabaseError, ProgrammingError, InternalError) as exc: - self._logger.debug("Failed to run reconcile", name=name, exc=exc) + self._logger.warning("Failed to run reconcile", name=name, exc=exc) class AuthentikBlueprintsConfig(ManagedAppConfig): 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/authenticated_sessions.py b/authentik/core/api/authenticated_sessions.py index 03c1aeaf3..2d77937be 100644 --- a/authentik/core/api/authenticated_sessions.py +++ b/authentik/core/api/authenticated_sessions.py @@ -14,7 +14,8 @@ from ua_parser import user_agent_parser from authentik.api.authorization import OwnerSuperuserPermissions from authentik.core.api.used_by import UsedByMixin from authentik.core.models import AuthenticatedSession -from authentik.events.geo import GEOIP_READER, GeoIPDict +from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict +from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict class UserAgentDeviceDict(TypedDict): @@ -59,6 +60,7 @@ class AuthenticatedSessionSerializer(ModelSerializer): current = SerializerMethodField() user_agent = SerializerMethodField() geo_ip = SerializerMethodField() + asn = SerializerMethodField() def get_current(self, instance: AuthenticatedSession) -> bool: """Check if session is currently active session""" @@ -70,8 +72,12 @@ class AuthenticatedSessionSerializer(ModelSerializer): return user_agent_parser.Parse(instance.last_user_agent) def get_geo_ip(self, instance: AuthenticatedSession) -> Optional[GeoIPDict]: # pragma: no cover - """Get parsed user agent""" - return GEOIP_READER.city_dict(instance.last_ip) + """Get GeoIP Data""" + return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip) + + def get_asn(self, instance: AuthenticatedSession) -> Optional[ASNDict]: # pragma: no cover + """Get ASN Data""" + return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip) class Meta: model = AuthenticatedSession @@ -80,6 +86,7 @@ class AuthenticatedSessionSerializer(ModelSerializer): "current", "user_agent", "geo_ip", + "asn", "user", "last_ip", "last_user_agent", 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/propertymappings.py b/authentik/core/api/propertymappings.py index 1e7436be9..d0fa7267b 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -19,6 +19,7 @@ from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer from authentik.core.expression.evaluator import PropertyMappingEvaluator from authentik.core.models import PropertyMapping +from authentik.enterprise.apps import EnterpriseConfig from authentik.events.utils import sanitize_item from authentik.lib.utils.reflection import all_subclasses from authentik.policies.api.exec import PolicyTestSerializer @@ -95,6 +96,7 @@ class PropertyMappingViewSet( "description": subclass.__doc__, "component": subclass().component, "model_name": subclass._meta.model_name, + "requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig), } ) return Response(TypeCreateSerializer(data, many=True).data) diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index a5095dcde..6c0f4db06 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -16,6 +16,7 @@ from rest_framework.viewsets import GenericViewSet from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.models import Provider +from authentik.enterprise.apps import EnterpriseConfig from authentik.lib.utils.reflection import all_subclasses @@ -113,6 +114,7 @@ class ProviderViewSet( "description": subclass.__doc__, "component": subclass().component, "model_name": subclass._meta.model_name, + "requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig), } ) data.append( diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 5ee249729..5b6a4a199 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 ( @@ -57,7 +51,7 @@ from authentik.admin.api.metrics import CoordinateSerializer from authentik.api.decorators import permission_required from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT 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, @@ -89,7 +83,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: @@ -110,7 +104,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..c79fec22e 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -2,7 +2,10 @@ from typing import Any from django.db.models import Model -from rest_framework.fields import CharField, IntegerField, JSONField +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 BooleanField, 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): @@ -56,6 +74,7 @@ class TypeCreateSerializer(PassiveSerializer): description = CharField(required=True) component = CharField(required=True) model_name = CharField(required=True) + requires_enterprise = BooleanField(default=False) class CacheSerializer(PassiveSerializer): diff --git a/authentik/core/channels.py b/authentik/core/channels.py index 00f213efc..722e9e03f 100644 --- a/authentik/core/channels.py +++ b/authentik/core/channels.py @@ -1,22 +1,29 @@ """Channels base classes""" +from channels.db import database_sync_to_async from channels.exceptions import DenyConnection -from channels.generic.websocket import JsonWebsocketConsumer from rest_framework.exceptions import AuthenticationFailed from structlog.stdlib import get_logger from authentik.api.authentication import bearer_auth -from authentik.core.models import User LOGGER = get_logger() -class AuthJsonConsumer(JsonWebsocketConsumer): +class TokenOutpostMiddleware: """Authorize a client with a token""" - user: User + def __init__(self, inner): + self.inner = inner - def connect(self): - headers = dict(self.scope["headers"]) + async def __call__(self, scope, receive, send): + scope = dict(scope) + await self.auth(scope) + return await self.inner(scope, receive, send) + + @database_sync_to_async + def auth(self, scope): + """Authenticate request from header""" + headers = dict(scope["headers"]) if b"authorization" not in headers: LOGGER.warning("WS Request without authorization header") raise DenyConnection() @@ -32,4 +39,4 @@ class AuthJsonConsumer(JsonWebsocketConsumer): LOGGER.warning("Failed to authenticate", exc=exc) raise DenyConnection() - self.user = user + scope["user"] = user diff --git a/authentik/core/expression/evaluator.py b/authentik/core/expression/evaluator.py index 85e6ccbc4..480caea21 100644 --- a/authentik/core/expression/evaluator.py +++ b/authentik/core/expression/evaluator.py @@ -44,6 +44,7 @@ class PropertyMappingEvaluator(BaseEvaluator): if request: req.http_request = request self._context["request"] = req + req.context.update(**kwargs) self._context.update(**kwargs) self.dry_run = dry_run diff --git a/authentik/core/models.py b/authentik/core/models.py index 7d11af3d6..125d5b0c8 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/base/skeleton.html b/authentik/core/templates/base/skeleton.html index 50a40de66..85137cc42 100644 --- a/authentik/core/templates/base/skeleton.html +++ b/authentik/core/templates/base/skeleton.html @@ -13,7 +13,6 @@ {% block head_before %} {% endblock %} - diff --git a/authentik/core/templates/login/base_full.html b/authentik/core/templates/login/base_full.html index 4cbf5e8dc..be6e3a040 100644 --- a/authentik/core/templates/login/base_full.html +++ b/authentik/core/templates/login/base_full.html @@ -6,6 +6,7 @@ {% block head_before %} + {% include "base/header_js.html" %} {% endblock %} @@ -43,28 +44,14 @@ {% block body %}
- - - - - - - - - - -
-