Compare commits

..

1 Commits

Author SHA1 Message Date
540c5864ee ci: push releases to new dockerhub
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 17:35:22 +01:00
413 changed files with 10774 additions and 28042 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.12.2 current_version = 2024.10.5
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -134,7 +134,7 @@ jobs:
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Create k8s Kind Cluster - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.12.0 uses: helm/kind-action@v1.11.0
- name: run integration - name: run integration
run: | run: |
poetry run coverage run manage.py test tests/integration poetry run coverage run manage.py test tests/integration
@ -168,8 +168,6 @@ jobs:
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap - name: ldap
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap* glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: rac
glob: tests/e2e/test_provider_rac*
- name: radius - name: radius
glob: tests/e2e/test_provider_radius* glob: tests/e2e/test_provider_radius*
- name: scim - name: scim
@ -245,7 +243,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables

View File

@ -82,7 +82,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables

View File

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -26,12 +26,17 @@ jobs:
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/server,beryju/authentik image-name: ghcr.io/goauthentik/server,beryju/authentik,authentik/server
- name: Docker Login Registry - name: Login to Docker Registry (legacy)
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Docker Registry (org)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@ -83,7 +88,7 @@ jobs:
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -92,16 +97,21 @@ jobs:
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }},authentik/${{ matrix.type }}
- name: make empty clients - name: make empty clients
run: | run: |
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api mkdir -p ./gen-go-api
- name: Docker Login Registry - name: Login to Docker Registry (legacy)
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Docker Registry (org)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@ -188,8 +198,8 @@ jobs:
aws-region: ${{ env.AWS_REGION }} aws-region: ${{ env.AWS_REGION }}
- name: Upload template - name: Upload template
run: | run: |
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
test-release: test-release:
needs: needs:
- build-server - build-server

View File

@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported | | Version | Supported |
| --------- | --------- | | --------- | --------- |
| 2024.8.x | ✅ |
| 2024.10.x | ✅ | | 2024.10.x | ✅ |
| 2024.12.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2024.12.2" __version__ = "2024.10.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
@ -16,5 +16,5 @@ def get_full_version() -> str:
"""Get full version, with build hash appended""" """Get full version, with build hash appended"""
version = __version__ version = __version__
if (build_hash := get_build_hash()) != "": if (build_hash := get_build_hash()) != "":
return f"{version}+{build_hash}" version += "." + build_hash
return version return version

View File

@ -7,9 +7,7 @@ from sys import version as python_version
from typing import TypedDict from typing import TypedDict
from cryptography.hazmat.backends.openssl.backend import backend from cryptography.hazmat.backends.openssl.backend import backend
from django.conf import settings
from django.utils.timezone import now from django.utils.timezone import now
from django.views.debug import SafeExceptionReporterFilter
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
@ -54,16 +52,10 @@ class SystemInfoSerializer(PassiveSerializer):
def get_http_headers(self, request: Request) -> dict[str, str]: def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers""" """Get HTTP Request headers"""
headers = {} headers = {}
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
for key, value in request.META.items(): for key, value in request.META.items():
if not isinstance(value, str): if not isinstance(value, str):
continue continue
actual_value = value headers[key] = value
if raw_session in actual_value:
actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute
)
headers[key] = actual_value
return headers return headers
def get_http_host(self, request: Request) -> str: def get_http_host(self, request: Request) -> str:

View File

@ -1,16 +1,12 @@
"""authentik administration overview""" """authentik administration overview"""
from socket import gethostname
from django.conf import settings from django.conf import settings
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import extend_schema, inline_serializer
from packaging.version import parse from rest_framework.fields import IntegerField
from rest_framework.fields import BooleanField, CharField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik import get_full_version
from authentik.rbac.permissions import HasPermission from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -20,38 +16,11 @@ class WorkerView(APIView):
permission_classes = [HasPermission("authentik_rbac.view_system_info")] permission_classes = [HasPermission("authentik_rbac.view_system_info")]
@extend_schema( @extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
responses=inline_serializer(
"Worker",
fields={
"worker_id": CharField(),
"version": CharField(),
"version_matching": BooleanField(),
},
many=True,
)
)
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:
"""Get currently connected worker count.""" """Get currently connected worker count."""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) count = len(CELERY_APP.control.ping(timeout=0.5))
our_version = parse(get_full_version())
response = []
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == our_version
response.append(
{"worker_id": key, "version": version, "version_matching": version_matching}
)
# In debug we run with `task_always_eager`, so tasks are ran on the main process # In debug we run with `task_always_eager`, so tasks are ran on the main process
if settings.DEBUG: # pragma: no cover if settings.DEBUG: # pragma: no cover
response.append( count += 1
{ return Response({"count": count})
"worker_id": f"authentik-debug@{gethostname()}",
"version": get_full_version(),
"version_matching": True,
}
)
return Response(response)

View File

@ -1,10 +1,11 @@
"""authentik admin app config""" """authentik admin app config"""
from prometheus_client import Info from prometheus_client import Gauge, Info
from authentik.blueprints.apps import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version") PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class AuthentikAdminConfig(ManagedAppConfig): class AuthentikAdminConfig(ManagedAppConfig):

View File

@ -1,35 +1,14 @@
"""admin signals""" """admin signals"""
from django.dispatch import receiver from django.dispatch import receiver
from packaging.version import parse
from prometheus_client import Gauge
from authentik import get_full_version from authentik.admin.apps import GAUGE_WORKERS
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.root.monitoring import monitoring_set from authentik.root.monitoring import monitoring_set
GAUGE_WORKERS = Gauge(
"authentik_admin_workers",
"Currently connected workers, their versions and if they are the same version as authentik",
["version", "version_matched"],
)
_version = parse(get_full_version())
@receiver(monitoring_set) @receiver(monitoring_set)
def monitoring_set_workers(sender, **kwargs): def monitoring_set_workers(sender, **kwargs):
"""Set worker gauge""" """Set worker gauge"""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) count = len(CELERY_APP.control.ping(timeout=0.5))
worker_version_count = {} GAUGE_WORKERS.set(count)
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == _version
worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
worker_version_count[version]["count"] += 1
for version, stats in worker_version_count.items():
GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])

View File

@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
response = self.client.get(reverse("authentik_api:admin_workers")) response = self.client.get(reverse("authentik_api:admin_workers"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
body = loads(response.content) body = loads(response.content)
self.assertEqual(len(body), 0) self.assertEqual(body["count"], 0)
def test_metrics(self): def test_metrics(self):
"""Test metrics API""" """Test metrics API"""

View File

@ -0,0 +1,67 @@
"""API Authorization"""
from django.conf import settings
from django.db.models import Model
from django.db.models.query import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.authentication import get_authorization_header
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from authentik.api.authentication import validate_auth
from authentik.rbac.filters import ObjectFilter
class OwnerFilter(BaseFilterBackend):
"""Filter objects by their owner"""
owner_key = "user"
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
if request.user.is_superuser:
return queryset
return queryset.filter(**{self.owner_key: request.user})
class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)
class OwnerPermissions(BasePermission):
"""Authorize requests by an object's owner matching the requesting user"""
owner_key = "user"
def has_permission(self, request: Request, view) -> bool:
"""If the user is authenticated, we allow all requests here. For listing, the
object-level permissions are done by the filter backend"""
return request.user.is_authenticated
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
"""Check if the object's owner matches the currently logged in user"""
if not hasattr(obj, self.owner_key):
return False
owner = getattr(obj, self.owner_key)
if owner != request.user:
return False
return True
class OwnerSuperuserPermissions(OwnerPermissions):
"""Similar to OwnerPermissions, except always allow access for superusers"""
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
if request.user.is_superuser:
return True
return super().has_object_permission(request, view, obj)

View File

@ -1,68 +0,0 @@
"""Test and debug Blueprints"""
import atexit
import readline
from pathlib import Path
from pprint import pformat
from sys import exit as sysexit
from textwrap import indent
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from yaml import load
from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
from authentik.core.management.commands.shell import get_banner_text
from authentik.lib.utils.errors import exception_to_string
LOGGER = get_logger()
class Command(BaseCommand):
"""Test and debug Blueprints"""
lines = []
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
histfolder = Path("~").expanduser() / Path(".local/share/authentik")
histfolder.mkdir(parents=True, exist_ok=True)
histfile = histfolder / Path("blueprint_shell_history")
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set editing-mode vi")
try:
readline.read_history_file(str(histfile))
except FileNotFoundError:
pass
atexit.register(readline.write_history_file, str(histfile))
@no_translations
def handle(self, *args, **options):
"""Interactively debug blueprint files"""
self.stdout.write(get_banner_text("Blueprint shell"))
self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
def do_eval():
yaml_input = "\n".join([line for line in self.lines if line])
data = load(yaml_input, BlueprintLoader)
self.stdout.write(pformat(data))
self.lines = []
while True:
try:
line = input("> ")
if line == ".eval":
do_eval()
else:
self.lines.append(line)
except EntryInvalidError as exc:
self.stdout.write("Failed to evaluate expression:")
self.stdout.write(indent(exception_to_string(exc), prefix=" "))
except EOFError:
break
except KeyboardInterrupt:
self.stdout.write()
sysexit(0)
self.stdout.write()

View File

@ -126,7 +126,7 @@ class Command(BaseCommand):
def_name_perm = f"model_{model_path}_permissions" def_name_perm = f"model_{model_path}_permissions"
def_path_perm = f"#/$defs/{def_name_perm}" def_path_perm = f"#/$defs/{def_name_perm}"
self.schema["$defs"][def_name_perm] = self.model_permissions(model) self.schema["$defs"][def_name_perm] = self.model_permissions(model)
template = { return {
"type": "object", "type": "object",
"required": ["model", "identifiers"], "required": ["model", "identifiers"],
"properties": { "properties": {
@ -143,11 +143,6 @@ class Command(BaseCommand):
"identifiers": {"$ref": def_path}, "identifiers": {"$ref": def_path},
}, },
} }
# Meta models don't require identifiers, as there's no matching database model to find
if issubclass(model, BaseMetaModel):
del template["properties"]["identifiers"]
template["required"].remove("identifiers")
return template
def field_to_jsonschema(self, field: Field) -> dict: def field_to_jsonschema(self, field: Field) -> dict:
"""Convert a single field to json schema""" """Convert a single field to json schema"""

View File

@ -202,9 +202,6 @@ class Blueprint:
class YAMLTag: class YAMLTag:
"""Base class for all YAML Tags""" """Base class for all YAML Tags"""
def __repr__(self) -> str:
return str(self.resolve(BlueprintEntry(""), Blueprint()))
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
"""Implement yaml tag logic""" """Implement yaml tag logic"""
raise NotImplementedError raise NotImplementedError

View File

@ -14,10 +14,10 @@ from rest_framework.response import Response
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.rbac.filters import SecretKeyFilter
from authentik.tenants.utils import get_current_tenant from authentik.tenants.utils import get_current_tenant

View File

@ -1,16 +1,15 @@
"""Application Roles API Viewset""" """Application Roles API Viewset"""
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.core.models import ( from authentik.core.models import (
Application, Application,
ApplicationEntitlement, ApplicationEntitlement,
User,
) )
@ -19,10 +18,7 @@ class ApplicationEntitlementSerializer(ModelSerializer):
def validate_app(self, app: Application) -> Application: def validate_app(self, app: Application) -> Application:
"""Ensure user has permission to view""" """Ensure user has permission to view"""
request: HttpRequest = self.context.get("request") user: User = self._context["request"].user
if not request and SERIALIZER_CONTEXT_BLUEPRINT in self.context:
return app
user = request.user
if user.has_perm("view_application", app) or user.has_perm( if user.has_perm("view_application", app) or user.has_perm(
"authentik_core.view_application" "authentik_core.view_application"
): ):

View File

@ -2,12 +2,16 @@
from typing import TypedDict from typing import TypedDict
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
@ -106,4 +110,11 @@ class AuthenticatedSessionViewSet(
search_fields = ["user__username", "last_ip", "last_user_agent"] search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"] filterset_fields = ["user__username", "last_ip", "last_user_agent"]
ordering = ["user__username"] ordering = ["user__username"]
owner_field = "user" permission_classes = [OwnerSuperuserPermissions]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -2,16 +2,19 @@
from collections.abc import Iterable from collections.abc import Iterable
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -186,10 +189,11 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all() queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["user", "source__slug"] filterset_fields = ["user", "source__slug"]
search_fields = ["source__slug"] search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"] ordering = ["source__slug", "pk"]
owner_field = "user"
class GroupSourceConnectionSerializer(SourceSerializer): class GroupSourceConnectionSerializer(SourceSerializer):
@ -224,7 +228,8 @@ class GroupSourceConnectionViewSet(
queryset = GroupSourceConnection.objects.all() queryset = GroupSourceConnection.objects.all()
serializer_class = GroupSourceConnectionSerializer serializer_class = GroupSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["group", "source__slug"] filterset_fields = ["group", "source__slug"]
search_fields = ["source__slug"] search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"] ordering = ["source__slug", "pk"]
owner_field = "user"

View File

@ -3,15 +3,18 @@
from typing import Any from typing import Any
from django.utils.timezone import now from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -135,8 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
"managed", "managed",
] ]
ordering = ["identifier", "expires"] ordering = ["identifier", "expires"]
owner_field = "user" permission_classes = [OwnerSuperuserPermissions]
rbac_allow_create_without_perm = True filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()

View File

@ -585,7 +585,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""Set password for user""" """Set password for user"""
user: User = self.get_object() user: User = self.get_object()
try: try:
user.set_password(request.data.get("password"), request=request) user.set_password(request.data.get("password"))
user.save() user.save()
except (ValidationError, IntegrityError) as exc: except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc) LOGGER.debug("Failed to set password", exc=exc)

View File

@ -44,12 +44,13 @@ class TokenBackend(InbuiltBackend):
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
) -> User | None: ) -> User | None:
try: try:
user = User._default_manager.get_by_natural_key(username) user = User._default_manager.get_by_natural_key(username)
except User.DoesNotExist: except User.DoesNotExist:
# Run the default password hasher once to reduce the timing # Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760). # difference between an existing and a nonexistent user (#20760).
User().set_password(password, request=request) User().set_password(password)
return None return None
tokens = Token.filter_not_expired( tokens = Token.filter_not_expired(

View File

@ -58,7 +58,6 @@ class PropertyMappingEvaluator(BaseEvaluator):
self._context["user"] = user self._context["user"] = user
if request: if request:
req.http_request = request req.http_request = request
self._context["http_request"] = request
req.context.update(**kwargs) req.context.update(**kwargs)
self._context["request"] = req self._context["request"] = req
self._context.update(**kwargs) self._context.update(**kwargs)

View File

@ -17,9 +17,7 @@ from authentik.events.middleware import should_log_model
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
def get_banner_text(shell_type="shell") -> str:
return f"""### authentik {shell_type} ({get_full_version()})
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """ ### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
@ -116,4 +114,4 @@ class Command(BaseCommand):
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
# Run interactive shell # Run interactive shell
code.interact(banner=get_banner_text(), local=namespace) code.interact(banner=BANNER_TEXT, local=namespace)

View File

@ -1,45 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0041_applicationentitlement"),
]
operations = [
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
),
),
]

View File

@ -356,13 +356,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user""" """superuser == staff user"""
return self.is_superuser # type: ignore return self.is_superuser # type: ignore
def set_password(self, raw_password, signal=True, sender=None, request=None): def set_password(self, raw_password, signal=True, sender=None):
if self.pk and signal: if self.pk and signal:
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
if not sender: if not sender:
sender = self sender = self
password_changed.send(sender=sender, user=self, password=raw_password, request=request) password_changed.send(sender=sender, user=self, password=raw_password)
self.password_change_date = now() self.password_change_date = now()
return super().set_password(raw_password) return super().set_password(raw_password)
@ -846,11 +846,6 @@ class ExpiringModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
indexes = [
models.Index(fields=["expires"]),
models.Index(fields=["expiring"]),
models.Index(fields=["expiring", "expires"]),
]
def expire_action(self, *args, **kwargs): def expire_action(self, *args, **kwargs):
"""Handler which is called when this object is expired. By """Handler which is called when this object is expired. By
@ -906,7 +901,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
class Meta: class Meta:
verbose_name = _("Token") verbose_name = _("Token")
verbose_name_plural = _("Tokens") verbose_name_plural = _("Tokens")
indexes = ExpiringModel.Meta.indexes + [ indexes = [
models.Index(fields=["identifier"]), models.Index(fields=["identifier"]),
models.Index(fields=["key"]), models.Index(fields=["key"]),
] ]
@ -1006,9 +1001,6 @@ class AuthenticatedSession(ExpiringModel):
class Meta: class Meta:
verbose_name = _("Authenticated Session") verbose_name = _("Authenticated Session")
verbose_name_plural = _("Authenticated Sessions") verbose_name_plural = _("Authenticated Sessions")
indexes = ExpiringModel.Meta.indexes + [
models.Index(fields=["session_key"]),
]
def __str__(self) -> str: def __str__(self) -> str:
return f"Authenticated Session {self.session_key[:10]}" return f"Authenticated Session {self.session_key[:10]}"

View File

@ -28,6 +28,7 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY from authentik.crypto.apps import MANAGED_KEY
@ -35,7 +36,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -1,27 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_enterprise", "0003_remove_licenseusage_within_limits_and_more"),
]
operations = [
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expires"], name="authentik_e_expires_3f2956_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_11d3d7_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_4d558f_idx"
),
),
]

View File

@ -93,4 +93,3 @@ class LicenseUsage(ExpiringModel):
class Meta: class Meta:
verbose_name = _("License Usage") verbose_name = _("License Usage")
verbose_name_plural = _("License Usage Records") verbose_name_plural = _("License Usage Records")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,8 +1,11 @@
"""RAC Provider API Views""" """RAC Provider API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
@ -31,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
] ]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet( class ConnectionTokenViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
@ -46,4 +55,10 @@ class ConnectionTokenViewSet(
filterset_fields = ["endpoint", "session__user", "provider"] filterset_fields = ["endpoint", "session__user", "provider"]
search_fields = ["endpoint__name", "provider__name"] search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"] ordering = ["endpoint__name", "provider__name"]
owner_field = "session__user" permission_classes = [OwnerSuperuserPermissions]
filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_providers_rac", "0005_alter_racpropertymapping_options"),
]
operations = [
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_91f148_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_59a5a7_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_aed3ca_idx"
),
),
]

View File

@ -159,9 +159,9 @@ class ConnectionToken(ExpiringModel):
default_settings["port"] = str(port) default_settings["port"] = str(port)
else: else:
default_settings["hostname"] = self.endpoint.host default_settings["hostname"] = self.endpoint.host
if self.endpoint.protocol == Protocols.RDP: default_settings["client-name"] = "authentik"
default_settings["resize-method"] = "display-update" # default_settings["enable-drive"] = "true"
default_settings["client-name"] = f"authentik - {self.session.user}" # default_settings["drive-name"] = "authentik"
settings = {} settings = {}
always_merger.merge(settings, default_settings) always_merger.merge(settings, default_settings)
always_merger.merge(settings, self.endpoint.provider.settings) always_merger.merge(settings, self.endpoint.provider.settings)
@ -211,4 +211,3 @@ class ConnectionToken(ExpiringModel):
class Meta: class Meta:
verbose_name = _("RAC Connection token") verbose_name = _("RAC Connection token")
verbose_name_plural = _("RAC Connection tokens") verbose_name_plural = _("RAC Connection tokens")
indexes = ExpiringModel.Meta.indexes

View File

@ -50,10 +50,9 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"resize-method": "display-update",
}, },
) )
# Set settings in provider # Set settings in provider
@ -64,11 +63,10 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"level": "provider", "level": "provider",
"resize-method": "display-update",
}, },
) )
# Set settings in endpoint # Set settings in endpoint
@ -81,11 +79,10 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"level": "endpoint", "level": "endpoint",
"resize-method": "display-update",
}, },
) )
# Set settings in token # Set settings in token
@ -98,11 +95,10 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"level": "token", "level": "token",
"resize-method": "display-update",
}, },
) )
# Set settings in property mapping (provider) # Set settings in property mapping (provider)
@ -118,11 +114,10 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"level": "property_mapping_provider", "level": "property_mapping_provider",
"resize-method": "display-update",
}, },
) )
# Set settings in property mapping (endpoint) # Set settings in property mapping (endpoint)
@ -140,12 +135,11 @@ class TestModels(TransactionTestCase):
{ {
"hostname": self.endpoint.host.split(":")[0], "hostname": self.endpoint.host.split(":")[0],
"port": "1324", "port": "1324",
"client-name": f"authentik - {self.user}", "client-name": "authentik",
"drive-path": path, "drive-path": path,
"create-drive-path": "true", "create-drive-path": "true",
"level": "property_mapping_endpoint", "level": "property_mapping_endpoint",
"foo": "true", "foo": "true",
"bar": "6", "bar": "6",
"resize-method": "display-update",
}, },
) )

View File

@ -1,11 +1,14 @@
"""AuthenticatorEndpointGDTCStage API Views""" """AuthenticatorEndpointGDTCStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
@ -64,7 +67,8 @@ class EndpointDeviceViewSet(
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class EndpointAdminDeviceViewSet(ModelViewSet): class EndpointAdminDeviceViewSet(ModelViewSet):

View File

@ -1,15 +1,17 @@
"""Notification API Views""" """Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.permissions import IsAuthenticated from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.events.api.events import EventSerializer from authentik.events.api.events import EventSerializer
@ -55,7 +57,8 @@ class NotificationViewSet(
"seen", "seen",
"user", "user",
] ]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
@extend_schema( @extend_schema(
request=OpenApiTypes.NONE, request=OpenApiTypes.NONE,
@ -63,7 +66,7 @@ class NotificationViewSet(
204: OpenApiResponse(description="Marked tasks as read successfully."), 204: OpenApiResponse(description="Marked tasks as read successfully."),
}, },
) )
@action(detail=False, methods=["post"], permission_classes=[IsAuthenticated]) @action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response: def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen""" """Mark all the user's notifications as seen"""
Notification.objects.filter(user=request.user, seen=False).update(seen=True) Notification.objects.filter(user=request.user, seen=False).update(seen=True)

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
]
operations = [
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expires"], name="authentik_e_expires_8c73a8_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_b5cb5e_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_e37180_idx"
),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expires"], name="authentik_e_expires_4d3985_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_81d649_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_eb3598_idx"
),
),
]

View File

@ -306,7 +306,7 @@ class Event(SerializerModel, ExpiringModel):
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
verbose_name_plural = _("Events") verbose_name_plural = _("Events")
indexes = ExpiringModel.Meta.indexes + [ indexes = [
models.Index(fields=["action"]), models.Index(fields=["action"]),
models.Index(fields=["user"]), models.Index(fields=["user"]),
models.Index(fields=["app"]), models.Index(fields=["app"]),
@ -694,4 +694,3 @@ class SystemTask(SerializerModel, ExpiringModel):
permissions = [("run_task", _("Run task"))] permissions = [("run_task", _("Run task"))]
verbose_name = _("System Task") verbose_name = _("System Task")
verbose_name_plural = _("System Tasks") verbose_name_plural = _("System Tasks")
indexes = ExpiringModel.Meta.indexes

View File

@ -106,9 +106,9 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
@receiver(password_changed) @receiver(password_changed)
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_): def on_password_changed(sender, user: User, password: str, **_):
"""Log password change""" """Log password change"""
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user) Event.new(EventAction.PASSWORD_SET).from_http(None, user=user)
@receiver(post_save, sender=Event) @receiver(post_save, sender=Event)

View File

@ -138,6 +138,7 @@ def notification_cleanup(self: SystemTask):
"""Cleanup seen notifications and notifications whose event expired.""" """Cleanup seen notifications and notifications whose event expired."""
notifications = Notification.objects.filter(Q(event=None) | Q(seen=True)) notifications = Notification.objects.filter(Q(event=None) | Q(seen=True))
amount = notifications.count() amount = notifications.count()
notifications.delete() for notification in notifications:
notification.delete()
LOGGER.debug("Expired notifications", amount=amount) LOGGER.debug("Expired notifications", amount=amount)
self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications") self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications")

View File

@ -1,7 +1,5 @@
"""Flow Stage API Views""" """Flow Stage API Views"""
from uuid import uuid4
from django.urls.base import reverse from django.urls.base import reverse
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import mixins from rest_framework import mixins
@ -29,11 +27,6 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField() component = SerializerMethodField()
flow_set = FlowSetSerializer(many=True, required=False) flow_set = FlowSetSerializer(many=True, required=False)
def to_representation(self, instance: Stage):
if isinstance(instance, Stage) and instance.is_in_memory:
instance.stage_uuid = uuid4()
return super().to_representation(instance)
def get_component(self, obj: Stage) -> str: def get_component(self, obj: Stage) -> str:
"""Get object type so that we know how to edit the object""" """Get object type so that we know how to edit the object"""
if obj.__class__ == Stage: if obj.__class__ == Stage:

View File

@ -88,8 +88,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding", model_name="flowstagebinding",
name="re_evaluate_policies", name="re_evaluate_policies",
field=models.BooleanField( field=models.BooleanField(
default=False, default=False, help_text="Evaluate policies when the Stage is present to the user."
help_text="Evaluate policies when the Stage is presented to the user.",
), ),
), ),
migrations.AddField( migrations.AddField(

View File

@ -20,7 +20,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding", model_name="flowstagebinding",
name="re_evaluate_policies", name="re_evaluate_policies",
field=models.BooleanField( field=models.BooleanField(
default=True, help_text="Evaluate policies when the Stage is presented to the user." default=True, help_text="Evaluate policies when the Stage is present to the user."
), ),
), ),
] ]

View File

@ -102,12 +102,8 @@ class Stage(SerializerModel):
user settings are available, or a challenge.""" user settings are available, or a challenge."""
return None return None
@property
def is_in_memory(self):
return hasattr(self, "__in_memory_type")
def __str__(self): def __str__(self):
if self.is_in_memory: if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}" return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return f"Stage {self.name}" return f"Stage {self.name}"
@ -231,7 +227,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
) )
re_evaluate_policies = models.BooleanField( re_evaluate_policies = models.BooleanField(
default=True, default=True,
help_text=_("Evaluate policies when the Stage is presented to the user."), help_text=_("Evaluate policies when the Stage is present to the user."),
) )
invalid_response_action = models.TextField( invalid_response_action = models.TextField(

View File

@ -159,17 +159,9 @@ class FlowPlan:
stage = final_stage(request=request, executor=temp_exec) stage = final_stage(request=request, executor=temp_exec)
return stage.dispatch(request) return stage.dispatch(request)
get_qs = request.GET.copy()
if request.user.is_authenticated and (
# Object-scoped permission or global permission
request.user.has_perm("authentik_flows.inspect_flow", flow)
or request.user.has_perm("authentik_flows.inspect_flow")
):
get_qs["inspector"] = "available"
return redirect_with_qs( return redirect_with_qs(
"authentik_core:if-flow", "authentik_core:if-flow",
get_qs, request.GET,
flow_slug=flow.slug, flow_slug=flow.slug,
) )

View File

@ -7,8 +7,8 @@ from django.http import HttpRequest, HttpResponse
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse from django.urls import reverse
from authentik.core.models import Group, User from authentik.core.models import User
from authentik.core.tests.utils import create_test_flow, create_test_user from authentik.core.tests.utils import create_test_flow
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import ( from authentik.flows.models import (
FlowDeniedAction, FlowDeniedAction,
@ -255,11 +255,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -282,8 +278,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -305,11 +301,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -318,11 +310,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding3 = FlowStageBinding.objects.create( binding3 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -340,9 +328,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker) self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -353,8 +341,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3) self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
# third request, this should trigger the re-evaluate # third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false # We do this request without the patch, so the policy results in false
@ -372,11 +360,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -385,11 +369,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding3 = FlowStageBinding.objects.create( binding3 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
@ -407,9 +387,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker) self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -420,8 +400,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3) self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
# Third request, this passes the first dummy stage # Third request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -431,7 +411,7 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding3) self.assertEqual(plan.bindings[0], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
# third request, this should trigger the re-evaluate # third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false # We do this request without the patch, so the policy results in false
@ -449,11 +429,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -468,11 +444,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding4 = FlowStageBinding.objects.create( binding4 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -493,10 +465,10 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.bindings[3], binding4) self.assertEqual(plan.bindings[3], binding4)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[2], ReevaluateMarker)
self.assertEqual(plan.markers[3].__class__, StageMarker) self.assertIsInstance(plan.markers[3], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -547,9 +519,9 @@ class TestFlowExecutor(FlowTestCase):
) )
# Stage 0 is a deny stage that is added dynamically # Stage 0 is a deny stage that is added dynamically
# when the reputation policy says so # when the reputation policy says so
deny_stage = DenyStage.objects.create(name=generate_id()) deny_stage = DenyStage.objects.create(name="deny")
reputation_policy = ReputationPolicy.objects.create( reputation_policy = ReputationPolicy.objects.create(
name=generate_id(), threshold=-1, check_ip=False name="reputation", threshold=-1, check_ip=False
) )
deny_binding = FlowStageBinding.objects.create( deny_binding = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -562,7 +534,7 @@ class TestFlowExecutor(FlowTestCase):
# Stage 1 is an identification stage # Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create( ident_stage = IdentificationStage.objects.create(
name=generate_id(), name="ident",
user_fields=[UserFields.E_MAIL], user_fields=[UserFields.E_MAIL],
pretend_user_exists=False, pretend_user_exists=False,
) )
@ -587,64 +559,3 @@ class TestFlowExecutor(FlowTestCase):
) )
response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True) response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied") self.assertStageResponse(response, flow, component="ak-stage-access-denied")
def test_re_evaluate_group_binding(self):
"""Test re-evaluate stage binding that has a policy binding to a group"""
flow = create_test_flow()
user_group_membership = create_test_user()
user_direct_binding = create_test_user()
user_other = create_test_user()
group_a = Group.objects.create(name=generate_id())
user_group_membership.ak_groups.add(group_a)
# Stage 0 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[UserFields.USERNAME],
pretend_user_exists=False,
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=0,
)
# Stage 1 is a dummy stage that is only shown for users in group_a
dummy_stage = DummyStage.objects.create(name=generate_id())
dummy_binding = FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
PolicyBinding.objects.create(group=group_a, target=dummy_binding, order=0)
PolicyBinding.objects.create(user=user_direct_binding, target=dummy_binding, order=0)
# Stage 2 is a deny stage that (in this case) only user_b will see
deny_stage = DenyStage.objects.create(name=generate_id())
FlowStageBinding.objects.create(target=flow, stage=deny_stage, order=2)
exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
with self.subTest(f"Test user access through group: {user_group_membership}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_group_membership.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user access through user: {user_direct_binding}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_direct_binding.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user has no access: {user_other}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied")

View File

@ -8,7 +8,6 @@ from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.lib.generators import generate_id
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
@ -27,7 +26,7 @@ class TestFlowInspector(APITestCase):
# Stage 1 is an identification stage # Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create( ident_stage = IdentificationStage.objects.create(
name=generate_id(), name="ident",
user_fields=[UserFields.USERNAME], user_fields=[UserFields.USERNAME],
) )
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
@ -36,8 +35,9 @@ class TestFlowInspector(APITestCase):
order=1, order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT, invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
) )
dummy_stage = DummyStage.objects.create(name=generate_id()) FlowStageBinding.objects.create(
FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1) target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
)
res = self.client.get( res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -68,11 +68,9 @@ class TestFlowInspector(APITestCase):
) )
content = loads(ins.content) content = loads(ins.content)
self.assertEqual(content["is_completed"], False) self.assertEqual(content["is_completed"], False)
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual( self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], ident_stage.name content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
)
self.assertEqual(
content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], dummy_stage.name
) )
self.client.post( self.client.post(
@ -86,12 +84,8 @@ class TestFlowInspector(APITestCase):
) )
content = loads(ins.content) content = loads(ins.content)
self.assertEqual(content["is_completed"], False) self.assertEqual(content["is_completed"], False)
self.assertEqual( self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
content["plans"][0]["current_stage"]["stage_obj"]["name"], ident_stage.name self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
)
self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], dummy_stage.name
)
self.assertEqual( self.assertEqual(
content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
) )

View File

@ -29,7 +29,6 @@ from authentik.flows.planner import (
cache_key, cache_key,
) )
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
@ -154,7 +153,7 @@ class TestFlowPlanner(TestCase):
"""Test planner cache""" """Test planner cache"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION) flow = create_test_flow(FlowDesignation.AUTHENTICATION)
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
) )
request = self.request_factory.get( request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -173,7 +172,7 @@ class TestFlowPlanner(TestCase):
"""Test planner with default_context""" """Test planner with default_context"""
flow = create_test_flow() flow = create_test_flow()
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
) )
user = User.objects.create(username="test-user") user = User.objects.create(username="test-user")
@ -192,7 +191,7 @@ class TestFlowPlanner(TestCase):
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, target=flow,
stage=DummyStage.objects.create(name=generate_id()), stage=DummyStage.objects.create(name="dummy1"),
order=0, order=0,
re_evaluate_policies=True, re_evaluate_policies=True,
) )
@ -205,7 +204,7 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
plan = planner.plan(request) plan = planner.plan(request)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], ReevaluateMarker)
def test_planner_reevaluate_actual(self): def test_planner_reevaluate_actual(self):
"""Test planner with re-evaluate""" """Test planner with re-evaluate"""
@ -213,14 +212,11 @@ class TestFlowPlanner(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
stage=DummyStage.objects.create(name=generate_id()), stage=DummyStage.objects.create(name="dummy2"),
order=1, order=1,
re_evaluate_policies=True, re_evaluate_policies=True,
) )
@ -244,8 +240,6 @@ class TestFlowPlanner(TestCase):
self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View File

@ -78,9 +78,7 @@ class FlowInspectorView(APIView):
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug) self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG: if settings.DEBUG:
return return
if request.user.has_perm( if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
"authentik_flows.inspect_flow", self.flow
) or request.user.has_perm("authentik_flows.inspect_flow"):
return return
raise Http404 raise Http404
@ -96,9 +94,6 @@ class FlowInspectorView(APIView):
"""Get current flow state and record it""" """Get current flow state and record it"""
plans = [] plans = []
for plan in request.session.get(SESSION_KEY_HISTORY, []): for plan in request.session.get(SESSION_KEY_HISTORY, []):
plan: FlowPlan
if plan.flow_pk != self.flow.pk.hex:
continue
plan_serializer = FlowInspectorPlanSerializer( plan_serializer = FlowInspectorPlanSerializer(
instance=plan, context={"request": request} instance=plan, context={"request": request}
) )

View File

@ -280,24 +280,9 @@ class ConfigLoader:
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc)) self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default return default
def get_optional_int(self, path: str, default=None) -> int | None:
"""Wrapper for get that converts value into int or None if set"""
value = self.get(path, default)
try:
return int(value)
except (ValueError, TypeError) as exc:
if value is None or (isinstance(value, str) and value.lower() == "null"):
return None
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
def get_bool(self, path: str, default=False) -> bool: def get_bool(self, path: str, default=False) -> bool:
"""Wrapper for get that converts value into boolean""" """Wrapper for get that converts value into boolean"""
value = self.get(path, UNSET) return str(self.get(path, default)).lower() == "true"
if value is UNSET:
return default
return str(self.get(path)).lower() == "true"
def get_keys(self, path: str, sep=".") -> list[str]: def get_keys(self, path: str, sep=".") -> list[str]:
"""List attribute keys by using yaml path""" """List attribute keys by using yaml path"""
@ -369,33 +354,20 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslcert": config.get("postgresql.sslcert"), "sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"), "sslkey": config.get("postgresql.sslkey"),
}, },
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
"postgresql.disable_server_side_cursors", False
),
"TEST": { "TEST": {
"NAME": config.get("postgresql.test.name"), "NAME": config.get("postgresql.test.name"),
}, },
} }
} }
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
if config.get_bool("postgresql.use_pgpool", False): if config.get_bool("postgresql.use_pgpool", False):
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
if disable_server_side_cursors is not UNSET:
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
if config.get_bool("postgresql.use_pgbouncer", False): if config.get_bool("postgresql.use_pgbouncer", False):
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors # https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections # https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
db["default"]["CONN_MAX_AGE"] = None # persistent db["default"]["CONN_MAX_AGE"] = None # persistent
if disable_server_side_cursors is not UNSET:
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
if conn_max_age is not UNSET:
db["default"]["CONN_MAX_AGE"] = conn_max_age
for replica in config.get_keys("postgresql.read_replicas"): for replica in config.get_keys("postgresql.read_replicas"):
_database = deepcopy(db["default"]) _database = deepcopy(db["default"])

View File

@ -6,6 +6,8 @@ postgresql:
user: authentik user: authentik
port: 5432 port: 5432
password: "env://POSTGRES_PASSWORD" password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false
use_pgpool: false
test: test:
name: test_authentik name: test_authentik
read_replicas: {} read_replicas: {}

View File

@ -9,25 +9,20 @@ from typing import Any
from cachetools import TLRUCache, cached from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.http import HttpRequest
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from sentry_sdk import start_span from sentry_sdk import start_span
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User from authentik.core.models import User
from authentik.events.models import Event from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import Policy, PolicyBinding from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
from authentik.stages.authenticator import devices_for_user from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger() LOGGER = get_logger()
@ -61,7 +56,6 @@ class BaseEvaluator:
"ak_logger": get_logger(self._filename).bind(), "ak_logger": get_logger(self._filename).bind(),
"ak_user_by": BaseEvaluator.expr_user_by, "ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator, "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ak_create_jwt": self.expr_create_jwt,
"ip_address": ip_address, "ip_address": ip_address,
"ip_network": ip_network, "ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten, "list_flatten": BaseEvaluator.expr_flatten,
@ -188,36 +182,6 @@ class BaseEvaluator:
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper() return proc.profiling_wrapper()
def expr_create_jwt(
self,
user: User,
provider: OAuth2Provider | str,
scopes: list[str],
validity: str = "seconds=60",
) -> str | None:
"""Issue a JWT for a given provider"""
request: HttpRequest = self._context.get("http_request")
if not request:
return None
if not isinstance(provider, OAuth2Provider):
provider = OAuth2Provider.objects.get(name=provider)
session = None
if hasattr(request, "session") and request.session.session_key:
session = AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).first()
access_token = AccessToken(
provider=provider,
user=user,
expires=now() + timedelta_from_string(validity),
scope=scopes,
auth_time=now(),
session=session,
)
access_token.id_token = IDToken.new(provider, access_token, request)
access_token.save()
return access_token.token
def wrap_expression(self, expression: str) -> str: def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`""" """Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys()) handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())

View File

@ -214,9 +214,6 @@ class TestConfig(TestCase):
"PORT": "foo", "PORT": "foo",
"TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
"USER": "foo", "USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
} }
}, },
) )
@ -254,9 +251,6 @@ class TestConfig(TestCase):
"PORT": "foo", "PORT": "foo",
"TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
"USER": "foo", "USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}, },
"replica_0": { "replica_0": {
"ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
@ -272,72 +266,6 @@ class TestConfig(TestCase):
"PORT": "foo", "PORT": "foo",
"TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
"USER": "foo", "USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
},
},
)
def test_db_read_replicas_pgbouncer(self):
"""Test read replicas"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.sslmode", "foo")
config.set("postgresql.sslrootcert", "foo")
config.set("postgresql.sslcert", "foo")
config.set("postgresql.sslkey", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pgbouncer", True)
# Read replica
config.set("postgresql.read_replicas.0.host", "bar")
# Override conn_max_age
config.set("postgresql.read_replicas.0.conn_max_age", 10)
# This isn't supported
config.set("postgresql.read_replicas.0.use_pgbouncer", False)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": None,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
"sslrootcert": "foo",
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
},
"replica_0": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 10,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
"sslrootcert": "foo",
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
}, },
}, },
) )
@ -366,8 +294,6 @@ class TestConfig(TestCase):
{ {
"default": { "default": {
"DISABLE_SERVER_SIDE_CURSORS": True, "DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
@ -384,8 +310,6 @@ class TestConfig(TestCase):
}, },
"replica_0": { "replica_0": {
"DISABLE_SERVER_SIDE_CURSORS": True, "DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
"HOST": "bar", "HOST": "bar",
"NAME": "foo", "NAME": "foo",
@ -438,9 +362,6 @@ class TestConfig(TestCase):
"PORT": "foo", "PORT": "foo",
"TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
"USER": "foo", "USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
}, },
"replica_0": { "replica_0": {
"ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
@ -456,9 +377,6 @@ class TestConfig(TestCase):
"PORT": "foo", "PORT": "foo",
"TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
"USER": "foo", "USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
}, },
}, },
) )

View File

@ -1,15 +1,11 @@
"""Test Evaluator base functions""" """Test Evaluator base functions"""
from django.test import RequestFactory, TestCase from django.test import TestCase
from django.urls import reverse
from jwt import decode
from authentik.blueprints.tests import apply_blueprint from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.events.models import Event from authentik.events.models import Event
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
class TestEvaluator(TestCase): class TestEvaluator(TestCase):
@ -45,35 +41,3 @@ class TestEvaluator(TestCase):
event = Event.objects.filter(action="custom_foo").first() event = Event.objects.filter(action="custom_foo").first()
self.assertIsNotNone(event) self.assertIsNotNone(event)
self.assertEqual(event.context, {"bar": "baz", "foo": "bar"}) self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})
@apply_blueprint("system/providers-oauth2.yaml")
def test_expr_create_jwt(self):
"""Test expr_create_jwt"""
rf = RequestFactory()
user = create_test_user()
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
managed__in=[
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
]
)
)
evaluator = BaseEvaluator(generate_id())
evaluator._context = {
"http_request": rf.get(reverse("authentik_core:root-redirect")),
"user": user,
"provider": provider.name,
}
jwt = evaluator.evaluate(
"return ak_create_jwt(user, provider, ['openid', 'email', 'profile'])"
)
decoded = decode(
jwt, provider.client_secret, algorithms=["HS256"], audience=provider.client_id
)
self.assertEqual(decoded["preferred_username"], user.username)

View File

@ -207,7 +207,7 @@ class KubernetesObjectReconciler(Generic[T]):
"app.kubernetes.io/instance": slugify(self.controller.outpost.name), "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
"app.kubernetes.io/managed-by": "goauthentik.io", "app.kubernetes.io/managed-by": "goauthentik.io",
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}", "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/version": get_version().replace("+", "-"), "app.kubernetes.io/version": get_version(),
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name), "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
"goauthentik.io/outpost-type": str(self.controller.outpost.type), "goauthentik.io/outpost-type": str(self.controller.outpost.type),
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,

View File

@ -94,7 +94,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
meta = self.get_object_meta(name=self.name) meta = self.get_object_meta(name=self.name)
image_name = self.controller.get_container_image() image_name = self.controller.get_container_image()
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
version = get_full_version().replace("+", "-") version = get_full_version()
return V1Deployment( return V1Deployment(
metadata=meta, metadata=meta,
spec=V1DeploymentSpec( spec=V1DeploymentSpec(

View File

@ -13,7 +13,7 @@ if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass(slots=True) @dataclass
class PrometheusServiceMonitorSpecEndpoint: class PrometheusServiceMonitorSpecEndpoint:
"""Prometheus ServiceMonitor endpoint spec""" """Prometheus ServiceMonitor endpoint spec"""
@ -21,14 +21,14 @@ class PrometheusServiceMonitorSpecEndpoint:
path: str = field(default="/metrics") path: str = field(default="/metrics")
@dataclass(slots=True) @dataclass
class PrometheusServiceMonitorSpecSelector: class PrometheusServiceMonitorSpecSelector:
"""Prometheus ServiceMonitor selector spec""" """Prometheus ServiceMonitor selector spec"""
matchLabels: dict matchLabels: dict
@dataclass(slots=True) @dataclass
class PrometheusServiceMonitorSpec: class PrometheusServiceMonitorSpec:
"""Prometheus ServiceMonitor spec""" """Prometheus ServiceMonitor spec"""
@ -37,7 +37,7 @@ class PrometheusServiceMonitorSpec:
selector: PrometheusServiceMonitorSpecSelector selector: PrometheusServiceMonitorSpecSelector
@dataclass(slots=True) @dataclass
class PrometheusServiceMonitorMetadata: class PrometheusServiceMonitorMetadata:
"""Prometheus ServiceMonitor metadata""" """Prometheus ServiceMonitor metadata"""
@ -46,7 +46,7 @@ class PrometheusServiceMonitorMetadata:
labels: dict = field(default_factory=dict) labels: dict = field(default_factory=dict)
@dataclass(slots=True) @dataclass
class PrometheusServiceMonitor: class PrometheusServiceMonitor:
"""Prometheus ServiceMonitor""" """Prometheus ServiceMonitor"""

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_policies_reputation",
"0007_reputation_authentik_p_identif_9434d7_idx_and_more",
),
]
operations = [
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["expires"], name="authentik_p_expires_da493f_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_2ab34f_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_2a8ec7_idx"
),
),
]

View File

@ -96,7 +96,7 @@ class Reputation(ExpiringModel, SerializerModel):
verbose_name = _("Reputation Score") verbose_name = _("Reputation Score")
verbose_name_plural = _("Reputation Scores") verbose_name_plural = _("Reputation Scores")
unique_together = ("identifier", "ip") unique_together = ("identifier", "ip")
indexes = ExpiringModel.Meta.indexes + [ indexes = [
models.Index(fields=["identifier"]), models.Index(fields=["identifier"]),
models.Index(fields=["ip"]), models.Index(fields=["ip"]),
models.Index(fields=["ip", "identifier"]), models.Index(fields=["ip", "identifier"]),

View File

@ -1,72 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_providers_oauth2", "0026_alter_accesstoken_session_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_9f24a5_idx"),
),
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_2d9205_idx"),
),
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_c74005_idx"
),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(fields=["expires"], name="authentik_p_expires_f594b2_idx"),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_6a5e2c_idx"),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_c0f353_idx"
),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_961437_idx"),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_4fd278_idx"),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_cd6b1c_idx"
),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_c479a7_idx"),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_d4d17f_idx"),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_acb4a5_idx"
),
),
]

View File

@ -425,7 +425,6 @@ class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel):
class Meta: class Meta:
verbose_name = _("Authorization Code") verbose_name = _("Authorization Code")
verbose_name_plural = _("Authorization Codes") verbose_name_plural = _("Authorization Codes")
indexes = ExpiringModel.Meta.indexes
def __str__(self): def __str__(self):
return f"Authorization code for {self.provider_id} for user {self.user_id}" return f"Authorization code for {self.provider_id} for user {self.user_id}"
@ -454,7 +453,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
_id_token = models.TextField() _id_token = models.TextField()
class Meta: class Meta:
indexes = ExpiringModel.Meta.indexes + [ indexes = [
HashIndex(fields=["token"]), HashIndex(fields=["token"]),
] ]
verbose_name = _("OAuth2 Access Token") verbose_name = _("OAuth2 Access Token")
@ -505,7 +504,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
) )
class Meta: class Meta:
indexes = ExpiringModel.Meta.indexes + [ indexes = [
HashIndex(fields=["token"]), HashIndex(fields=["token"]),
] ]
verbose_name = _("OAuth2 Refresh Token") verbose_name = _("OAuth2 Refresh Token")
@ -557,7 +556,6 @@ class DeviceToken(ExpiringModel):
class Meta: class Meta:
verbose_name = _("Device Token") verbose_name = _("Device Token")
verbose_name_plural = _("Device Tokens") verbose_name_plural = _("Device Tokens")
indexes = ExpiringModel.Meta.indexes
def __str__(self): def __str__(self):
return f"Device Token for {self.provider_id}" return f"Device Token for {self.provider_id}"

View File

@ -49,9 +49,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": self.device_flow.slug, "flow_slug": self.device_flow.slug,
}, },
) ),
+ "?"
+ urlencode({"inspector": "available"}),
) )
def test_device_init_post(self): def test_device_init_post(self):
@ -65,9 +63,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": self.device_flow.slug, "flow_slug": self.device_flow.slug,
}, },
) ),
+ "?"
+ urlencode({"inspector": "available"}),
) )
res = self.api_client.get( res = self.api_client.get(
reverse( reverse(
@ -122,9 +118,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={ kwargs={
"flow_slug": provider.authorization_flow.slug, "flow_slug": provider.authorization_flow.slug,
}, },
) ),
+ "?"
+ urlencode({"inspector": "available"}),
}, },
) )
@ -156,7 +150,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
}, },
) )
+ "?" + "?"
+ urlencode({QS_KEY_CODE: token.user_code, "inspector": "available"}), + urlencode({QS_KEY_CODE: token.user_code}),
) )
def test_device_init_denied(self): def test_device_init_denied(self):

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass(slots=True) @dataclass
class TraefikMiddlewareSpecForwardAuth: class TraefikMiddlewareSpecForwardAuth:
"""traefik middleware forwardAuth spec""" """traefik middleware forwardAuth spec"""
@ -28,14 +28,14 @@ class TraefikMiddlewareSpecForwardAuth:
trustForwardHeader: bool = field(default=True) trustForwardHeader: bool = field(default=True)
@dataclass(slots=True) @dataclass
class TraefikMiddlewareSpec: class TraefikMiddlewareSpec:
"""Traefik middleware spec""" """Traefik middleware spec"""
forwardAuth: TraefikMiddlewareSpecForwardAuth forwardAuth: TraefikMiddlewareSpecForwardAuth
@dataclass(slots=True) @dataclass
class TraefikMiddlewareMetadata: class TraefikMiddlewareMetadata:
"""Traefik Middleware metadata""" """Traefik Middleware metadata"""
@ -44,7 +44,7 @@ class TraefikMiddlewareMetadata:
labels: dict = field(default_factory=dict) labels: dict = field(default_factory=dict)
@dataclass(slots=True) @dataclass
class TraefikMiddleware: class TraefikMiddleware:
"""Traefik Middleware""" """Traefik Middleware"""

View File

@ -16,7 +16,6 @@ from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, SerializerMethodField from rest_framework.fields import CharField, FileField, SerializerMethodField
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.renderers import BaseRenderer, JSONRenderer
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError
@ -39,16 +38,6 @@ from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_
LOGGER = get_logger() LOGGER = get_logger()
class RawXMLDataRenderer(BaseRenderer):
"""Renderer to allow application/xml as value for 'Accept' in the metadata endpoint."""
media_type = "application/xml"
format = "xml"
def render(self, data, accepted_media_type=None, renderer_context=None):
return data
class SAMLProviderSerializer(ProviderSerializer): class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer""" """SAMLProvider Serializer"""
@ -65,22 +54,8 @@ class SAMLProviderSerializer(ProviderSerializer):
if "request" not in self._context: if "request" not in self._context:
return "" return ""
request: HttpRequest = self._context["request"]._request request: HttpRequest = self._context["request"]._request
try:
return request.build_absolute_uri( return request.build_absolute_uri(
reverse( reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk}) + "?download"
"authentik_providers_saml:metadata-download",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return request.build_absolute_uri(
reverse(
"authentik_api:samlprovider-metadata",
kwargs={
"pk": instance.pk,
},
)
+ "?download"
) )
def get_url_sso_post(self, instance: SAMLProvider) -> str: def get_url_sso_post(self, instance: SAMLProvider) -> str:
@ -249,21 +224,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
], ],
description="Optionally force the metadata to only include one binding.", description="Optionally force the metadata to only include one binding.",
), ),
# Explicitly excluded, because otherwise spectacular automatically
# add it when using multiple renderer_classes
OpenApiParameter(
name="format",
exclude=True,
required=False,
),
], ],
) )
@action( @action(methods=["GET"], detail=True, permission_classes=[AllowAny])
methods=["GET"],
detail=True,
permission_classes=[AllowAny],
renderer_classes=[JSONRenderer, RawXMLDataRenderer],
)
def metadata(self, request: Request, pk: int) -> Response: def metadata(self, request: Request, pk: int) -> Response:
"""Return metadata as XML string""" """Return metadata as XML string"""
# We don't use self.get_object() on purpose as this view is un-authenticated # We don't use self.get_object() on purpose as this view is un-authenticated
@ -281,9 +244,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
f'attachment; filename="{provider.name}_authentik_meta.xml"' f'attachment; filename="{provider.name}_authentik_meta.xml"'
) )
return response return response
return Response({"metadata": metadata}, content_type="application/json") return Response({"metadata": metadata})
except Provider.application.RelatedObjectDoesNotExist: except Provider.application.RelatedObjectDoesNotExist:
return Response({"metadata": ""}, content_type="application/json") return Response({"metadata": ""})
@permission_required( @permission_required(
None, None,

View File

@ -256,7 +256,7 @@ class AssertionProcessor:
assertion.attrib["IssueInstant"] = self._issue_instant assertion.attrib["IssueInstant"] = self._issue_instant
assertion.append(self.get_issuer()) assertion.append(self.get_issuer())
if self.provider.signing_kp and self.provider.sign_assertion: if self.provider.signing_kp:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get( sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1 self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
) )
@ -295,18 +295,6 @@ class AssertionProcessor:
response.append(self.get_issuer()) response.append(self.get_issuer())
if self.provider.signing_kp and self.provider.sign_response:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
response,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
response.append(signature)
status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status") status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode") status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success" status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success"

View File

@ -104,22 +104,6 @@ class TestSAMLProviderAPI(APITestCase):
) )
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response) self.assertIn("Content-Disposition", response)
# Test download with Accept: application/xml
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
+ "?download",
HTTP_ACCEPT="application/xml",
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
+ "?download",
HTTP_ACCEPT="application/xml;charset=UTF-8",
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_metadata_invalid(self): def test_metadata_invalid(self):
"""Test metadata export (invalid)""" """Test metadata export (invalid)"""
@ -137,11 +121,6 @@ class TestSAMLProviderAPI(APITestCase):
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}), reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
) )
self.assertEqual(404, response.status_code) self.assertEqual(404, response.status_code)
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
HTTP_ACCEPT="application/invalid-mime-type",
)
self.assertEqual(406, response.status_code)
def test_import_success(self): def test_import_success(self):
"""Test metadata import (success case)""" """Test metadata import (success case)"""

View File

@ -2,10 +2,8 @@
from base64 import b64encode from base64 import b64encode
from defusedxml.lxml import fromstring
from django.http.request import QueryDict from django.http.request import QueryDict
from django.test import TestCase from django.test import TestCase
from lxml import etree # nosec
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
@ -13,14 +11,12 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request from authentik.lib.tests.utils import get_request
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
from authentik.sources.saml.exceptions import MismatchedRequestID from authentik.sources.saml.exceptions import MismatchedRequestID
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
NS_MAP,
SAML_BINDING_REDIRECT, SAML_BINDING_REDIRECT,
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_UNSPECIFIED, SAML_NAME_ID_FORMAT_UNSPECIFIED,
@ -189,19 +185,6 @@ class TestAuthNRequest(TestCase):
self.assertEqual(response.count(response_proc._assertion_id), 2) self.assertEqual(response.count(response_proc._assertion_id), 2)
self.assertEqual(response.count(response_proc._response_id), 2) self.assertEqual(response.count(response_proc._response_id), 2)
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(lxml_from_string(response)))
response_xml = fromstring(response)
self.assertEqual(
len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1
)
self.assertEqual(
len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1
)
# Now parse the response (source) # Now parse the response (source)
http_request.POST = QueryDict(mutable=True) http_request.POST = QueryDict(mutable=True)
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()

View File

@ -5,7 +5,6 @@ from django.contrib.auth.models import Permission
from django.db.models import QuerySet from django.db.models import QuerySet
from django_filters.filters import ModelChoiceFilter from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import ( from rest_framework.fields import (
CharField, CharField,
@ -14,8 +13,6 @@ from rest_framework.fields import (
ReadOnlyField, ReadOnlyField,
SerializerMethodField, SerializerMethodField,
) )
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
@ -95,9 +92,7 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
queryset = Permission.objects.none() queryset = Permission.objects.none()
serializer_class = PermissionSerializer serializer_class = PermissionSerializer
ordering = ["name"] ordering = ["name"]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_class = PermissionFilter filterset_class = PermissionFilter
permission_classes = [IsAuthenticated]
search_fields = [ search_fields = [
"codename", "codename",
"content_type__model", "content_type__model",

View File

@ -1,15 +1,10 @@
"""RBAC API Filter""" """RBAC API Filter"""
from django.conf import settings
from django.db.models import QuerySet from django.db.models import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView
from rest_framework_guardian.filters import ObjectPermissionsFilter from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.api.authentication import validate_auth
from authentik.core.models import UserTypes from authentik.core.models import UserTypes
@ -17,7 +12,7 @@ class ObjectFilter(ObjectPermissionsFilter):
"""Object permission filter that grants global permission higher priority than """Object permission filter that grants global permission higher priority than
per-object permissions""" per-object permissions"""
def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView) -> QuerySet: def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
permission = self.perm_format % { permission = self.perm_format % {
"app_label": queryset.model._meta.app_label, "app_label": queryset.model._meta.app_label,
"model_name": queryset.model._meta.model_name, "model_name": queryset.model._meta.model_name,
@ -26,9 +21,6 @@ class ObjectFilter(ObjectPermissionsFilter):
# per-object permissions # per-object permissions
if request.user.has_perm(permission): if request.user.has_perm(permission):
return queryset return queryset
# User does not have permissions, but we have an owner field defined, so filter by that
if owner_field := getattr(view, "owner_field", None):
return queryset.filter(**{owner_field: request.user})
queryset = super().filter_queryset(request, queryset, view) queryset = super().filter_queryset(request, queryset, view)
# Outposts (which are the only objects using internal service accounts) # Outposts (which are the only objects using internal service accounts)
# except requests to return an empty list when they have no objects # except requests to return an empty list when they have no objects
@ -40,17 +32,3 @@ class ObjectFilter(ObjectPermissionsFilter):
# and also no object permissions assigned (directly or via role) # and also no object permissions assigned (directly or via role)
raise PermissionDenied() raise PermissionDenied()
return queryset return queryset
class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)

View File

@ -15,17 +15,6 @@ class ObjectPermissions(DjangoObjectPermissions):
lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None) lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None)
if lookup and lookup in view.kwargs: if lookup and lookup in view.kwargs:
return True return True
# Legacy behaviour:
# Allow creation of objects even without explicit permission
queryset = self._queryset(view)
required_perms = self.get_required_permissions(request.method, queryset.model)
if (
len(required_perms) == 1
and f"{queryset.model._meta.app_label}.add_{queryset.model._meta.model_name}"
in required_perms
and getattr(view, "rbac_allow_create_without_perm", False)
):
return True
return super().has_permission(request, view) return super().has_permission(request, view)
def has_object_permission(self, request: Request, view, obj: Model) -> bool: def has_object_permission(self, request: Request, view, obj: Model) -> bool:
@ -35,10 +24,6 @@ class ObjectPermissions(DjangoObjectPermissions):
# Rank global permissions higher than per-object permissions # Rank global permissions higher than per-object permissions
if request.user.has_perms(perms): if request.user.has_perms(perms):
return True return True
# Allow access for owners if configured
if owner_field := getattr(view, "owner_field", None):
if getattr(obj, owner_field) == request.user:
return True
return super().has_object_permission(request, view, obj) return super().has_object_permission(request, view, obj)

View File

@ -18,7 +18,6 @@ from celery.signals import (
task_prerun, task_prerun,
worker_ready, worker_ready,
) )
from celery.worker.control import inspect_command
from django.conf import settings from django.conf import settings
from django.db import ProgrammingError from django.db import ProgrammingError
from django_tenants.utils import get_public_schema_name from django_tenants.utils import get_public_schema_name
@ -26,7 +25,6 @@ from structlog.contextvars import STRUCTLOG_KEY_PREFIX
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
from authentik import get_full_version
from authentik.lib.sentry import before_send from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
@ -161,12 +159,6 @@ class LivenessProbe(bootsteps.StartStopStep):
HEARTBEAT_FILE.touch() HEARTBEAT_FILE.touch()
@inspect_command(default_timeout=0.2)
def ping(state, **kwargs):
"""Ping worker(s)."""
return {"ok": "pong", "version": get_full_version()}
CELERY_APP.config_from_object(settings.CELERY) CELERY_APP.config_from_object(settings.CELERY)
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.

View File

@ -31,8 +31,6 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("randomly_seed", None): if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}") self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
if kwargs.get("no_capture"):
self.args.append("-s")
settings.TEST = True settings.TEST = True
settings.CELERY["task_always_eager"] = True settings.CELERY["task_always_eager"] = True
@ -58,10 +56,6 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
def add_arguments(cls, parser: ArgumentParser): def add_arguments(cls, parser: ArgumentParser):
"""Add more pytest-specific arguments""" """Add more pytest-specific arguments"""
DiscoverRunner.add_arguments(parser) DiscoverRunner.add_arguments(parser)
parser.add_argument(
"--no-capture",
action="store_true",
)
parser.add_argument( parser.add_argument(
"--randomly-seed", "--randomly-seed",
type=int, type=int,

View File

@ -1,7 +1,10 @@
"""Kerberos Source Serializer""" """Kerberos Source Serializer"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.sources import ( from authentik.core.api.sources import (
GroupSourceConnectionSerializer, GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet, GroupSourceConnectionViewSet,
@ -29,8 +32,9 @@ class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
serializer_class = UserKerberosSourceConnectionSerializer serializer_class = UserKerberosSourceConnectionSerializer
filterset_fields = ["source__slug"] filterset_fields = ["source__slug"]
search_fields = ["source__slug"] search_fields = ["source__slug"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug"] ordering = ["source__slug"]
owner_field = "user"
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer): class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):

View File

@ -28,19 +28,17 @@ class KerberosBackend(InbuiltBackend):
if "@" in username: if "@" in username:
username, realm = username.rsplit("@", 1) username, realm = username.rsplit("@", 1)
user, source = self.auth_user(request, username, realm, **kwargs) user, source = self.auth_user(username, realm, **kwargs)
if user: if user:
self.set_method("kerberos", request, source=source) self.set_method("kerberos", request, source=source)
return user return user
return None return None
def auth_user( def auth_user(
self, request: HttpRequest, username: str, realm: str | None, password: str, **filters self, username: str, realm: str | None, password: str, **filters
) -> tuple[User | None, KerberosSource | None]: ) -> tuple[User | None, KerberosSource | None]:
sources = KerberosSource.objects.filter(enabled=True) sources = KerberosSource.objects.filter(enabled=True)
user = User.objects.filter( user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
usersourceconnection__source__in=sources, username=username, **filters
).first()
if user is not None: if user is not None:
# User found, let's get its connections for the sources that are available # User found, let's get its connections for the sources that are available
@ -76,10 +74,10 @@ class KerberosBackend(InbuiltBackend):
user=user_source_connection.user, user=user_source_connection.user,
) )
user_source_connection.user.set_password( user_source_connection.user.set_password(
password, sender=user_source_connection.source, request=request password, sender=user_source_connection.source
) )
user_source_connection.user.save() user_source_connection.user.save()
return user_source_connection.user, user_source_connection.source return user, user_source_connection.source
# Password doesn't match, onto next source # Password doesn't match, onto next source
LOGGER.debug( LOGGER.debug(
"failed to kinit, password invalid", "failed to kinit, password invalid",

View File

@ -20,15 +20,13 @@ class LDAPBackend(InbuiltBackend):
return None return None
for source in LDAPSource.objects.filter(enabled=True): for source in LDAPSource.objects.filter(enabled=True):
LOGGER.debug("LDAP Auth attempt", source=source) LOGGER.debug("LDAP Auth attempt", source=source)
user = self.auth_user(request, source, **kwargs) user = self.auth_user(source, **kwargs)
if user: if user:
self.set_method("ldap", request, source=source) self.set_method("ldap", request, source=source)
return user return user
return None return None
def auth_user( def auth_user(self, source: LDAPSource, password: str, **filters: str) -> User | None:
self, request: HttpRequest, source: LDAPSource, password: str, **filters: str
) -> User | None:
"""Try to bind as either user_dn or mail with password. """Try to bind as either user_dn or mail with password.
Returns True on success, otherwise False""" Returns True on success, otherwise False"""
users = User.objects.filter(**filters) users = User.objects.filter(**filters)
@ -45,7 +43,7 @@ class LDAPBackend(InbuiltBackend):
if source.password_login_update_internal_password: if source.password_login_update_internal_password:
# Password given successfully binds to LDAP, so we save it in our Database # Password given successfully binds to LDAP, so we save it in our Database
LOGGER.debug("Updating user's password in DB", user=user) LOGGER.debug("Updating user's password in DB", user=user)
user.set_password(password, sender=source, request=request) user.set_password(password, sender=source)
user.save() user.save()
return user return user
# Password doesn't match # Password doesn't match

View File

@ -88,55 +88,6 @@ class TestSCIMUsers(APITestCase):
).exists() ).exists()
) )
def test_user_create_duplicate_by_username(self):
"""Test user create"""
user = create_test_user()
username = generate_id()
obj1 = {
"userName": username,
"externalId": generate_id(),
"emails": [
{
"primary": True,
"value": user.email,
}
],
}
obj2 = obj1.copy()
obj2.update({"externalId": generate_id()})
response = self.client.post(
reverse(
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.source.slug,
},
),
data=dumps(obj1),
content_type=SCIM_CONTENT_TYPE,
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
)
self.assertEqual(response.status_code, 201)
self.assertTrue(
SCIMSourceUser.objects.filter(source=self.source, user__username=username).exists()
)
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
).exists()
)
response = self.client.post(
reverse(
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.source.slug,
},
),
data=dumps(obj2),
content_type=SCIM_CONTENT_TYPE,
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
)
self.assertEqual(response.status_code, 409)
def test_user_property_mappings(self): def test_user_property_mappings(self):
"""Test user property_mappings""" """Test user property_mappings"""
self.source.user_property_mappings.set( self.source.user_property_mappings.set(

View File

@ -2,7 +2,6 @@
from uuid import uuid4 from uuid import uuid4
from django.db.models import Q
from django.db.transaction import atomic from django.db.transaction import atomic
from django.http import Http404, QueryDict from django.http import Http404, QueryDict
from django.urls import reverse from django.urls import reverse
@ -114,11 +113,8 @@ class UsersView(SCIMObjectView):
def post(self, request: Request, **kwargs) -> Response: def post(self, request: Request, **kwargs) -> Response:
"""Create user handler""" """Create user handler"""
connection = SCIMSourceUser.objects.filter( connection = SCIMSourceUser.objects.filter(
Q(
Q(user__uuid=request.data.get("id"))
| Q(user__username=request.data.get("userName"))
),
source=self.source, source=self.source,
user__uuid=request.data.get("id"),
).first() ).first()
if connection: if connection:
self.logger.debug("Found existing user") self.logger.debug("Found existing user")

View File

@ -1,18 +1,20 @@
"""AuthenticatorDuoStage API Views""" """AuthenticatorDuoStage API Views"""
from django.http import Http404 from django.http import Http404
from django_filters.rest_framework.backends import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, IntegerField from rest_framework.fields import CharField, ChoiceField, IntegerField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupMemberSerializer from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
@ -166,11 +168,9 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
class DuoDeviceSerializer(ModelSerializer): class DuoDeviceSerializer(ModelSerializer):
"""Serializer for Duo authenticator devices""" """Serializer for Duo authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta: class Meta:
model = DuoDevice model = DuoDevice
fields = ["pk", "name", "user"] fields = ["pk", "name"]
depth = 2 depth = 2
@ -189,7 +189,8 @@ class DuoDeviceViewSet(
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class DuoAdminDeviceViewSet(ModelViewSet): class DuoAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorSMSStage API Views""" """AuthenticatorSMSStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
@ -42,11 +44,9 @@ class AuthenticatorSMSStageViewSet(UsedByMixin, ModelViewSet):
class SMSDeviceSerializer(ModelSerializer): class SMSDeviceSerializer(ModelSerializer):
"""Serializer for sms authenticator devices""" """Serializer for sms authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta: class Meta:
model = SMSDevice model = SMSDevice
fields = ["name", "pk", "phone_number", "user"] fields = ["name", "pk", "phone_number"]
depth = 2 depth = 2
extra_kwargs = { extra_kwargs = {
"phone_number": {"read_only": True}, "phone_number": {"read_only": True},
@ -65,10 +65,11 @@ class SMSDeviceViewSet(
queryset = SMSDevice.objects.all() queryset = SMSDevice.objects.all()
serializer_class = SMSDeviceSerializer serializer_class = SMSDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user"
class SMSAdminDeviceViewSet(ModelViewSet): class SMSAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorStaticStage API Views""" """AuthenticatorStaticStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
@ -49,11 +51,10 @@ class StaticDeviceSerializer(ModelSerializer):
"""Serializer for static authenticator devices""" """Serializer for static authenticator devices"""
token_set = StaticDeviceTokenSerializer(many=True, read_only=True) token_set = StaticDeviceTokenSerializer(many=True, read_only=True)
user = GroupMemberSerializer(read_only=True)
class Meta: class Meta:
model = StaticDevice model = StaticDevice
fields = ["name", "token_set", "pk", "user"] fields = ["name", "token_set", "pk"]
class StaticDeviceViewSet( class StaticDeviceViewSet(
@ -68,10 +69,11 @@ class StaticDeviceViewSet(
queryset = StaticDevice.objects.filter(confirmed=True) queryset = StaticDevice.objects.filter(confirmed=True)
serializer_class = StaticDeviceSerializer serializer_class = StaticDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user"
class StaticAdminDeviceViewSet(ModelViewSet): class StaticAdminDeviceViewSet(ModelViewSet):

View File

@ -1,10 +1,12 @@
"""AuthenticatorTOTPStage API Views""" """AuthenticatorTOTPStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import ChoiceField from rest_framework.fields import ChoiceField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
@ -38,14 +40,11 @@ class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
class TOTPDeviceSerializer(ModelSerializer): class TOTPDeviceSerializer(ModelSerializer):
"""Serializer for totp authenticator devices""" """Serializer for totp authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta: class Meta:
model = TOTPDevice model = TOTPDevice
fields = [ fields = [
"name", "name",
"pk", "pk",
"user",
] ]
depth = 2 depth = 2
@ -62,10 +61,11 @@ class TOTPDeviceViewSet(
queryset = TOTPDevice.objects.filter(confirmed=True) queryset = TOTPDevice.objects.filter(confirmed=True)
serializer_class = TOTPDeviceSerializer serializer_class = TOTPDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user"
class TOTPAdminDeviceViewSet(ModelViewSet): class TOTPAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorWebAuthnStage API Views""" """AuthenticatorWebAuthnStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
@ -14,11 +16,10 @@ class WebAuthnDeviceSerializer(ModelSerializer):
"""Serializer for WebAuthn authenticator devices""" """Serializer for WebAuthn authenticator devices"""
device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True) device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True)
user = GroupMemberSerializer(read_only=True)
class Meta: class Meta:
model = WebAuthnDevice model = WebAuthnDevice
fields = ["pk", "name", "created_on", "device_type", "aaguid", "user"] fields = ["pk", "name", "created_on", "device_type", "aaguid"]
extra_kwargs = { extra_kwargs = {
"aaguid": {"read_only": True}, "aaguid": {"read_only": True},
} }
@ -39,7 +40,8 @@ class WebAuthnDeviceViewSet(
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class WebAuthnAdminDeviceViewSet(ModelViewSet): class WebAuthnAdminDeviceViewSet(ModelViewSet):

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,12 @@
"""ConsentStage API Views""" """ConsentStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.applications import ApplicationSerializer from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
@ -53,4 +57,11 @@ class UserConsentViewSet(
filterset_fields = ["user", "application"] filterset_fields = ["user", "application"]
ordering = ["application", "expires"] ordering = ["application", "expires"]
search_fields = ["user__username"] search_fields = ["user__username"]
owner_field = "user" permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_stages_consent", "0006_alter_userconsent_expires"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="userconsent",
index=models.Index(fields=["expires"], name="authentik_s_expires_0e99e8_idx"),
),
migrations.AddIndex(
model_name="userconsent",
index=models.Index(fields=["expiring"], name="authentik_s_expirin_8f51e5_idx"),
),
migrations.AddIndex(
model_name="userconsent",
index=models.Index(
fields=["expiring", "expires"], name="authentik_s_expirin_e50090_idx"
),
),
]

View File

@ -71,4 +71,3 @@ class UserConsent(SerializerModel, ExpiringModel):
unique_together = (("user", "application", "permissions"),) unique_together = (("user", "application", "permissions"),)
verbose_name = _("User Consent") verbose_name = _("User Consent")
verbose_name_plural = _("User Consents") verbose_name_plural = _("User Consents")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0027_auto_20231028_1424"),
("authentik_stages_invitation", "0008_alter_invitation_expires"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="invitation",
index=models.Index(fields=["expires"], name="authentik_s_expires_96f4b8_idx"),
),
migrations.AddIndex(
model_name="invitation",
index=models.Index(fields=["expiring"], name="authentik_s_expirin_4f8f35_idx"),
),
migrations.AddIndex(
model_name="invitation",
index=models.Index(
fields=["expiring", "expires"], name="authentik_s_expirin_4f8096_idx"
),
),
]

View File

@ -84,4 +84,3 @@ class Invitation(SerializerModel, ExpiringModel):
class Meta: class Meta:
verbose_name = _("Invitation") verbose_name = _("Invitation")
verbose_name_plural = _("Invitations") verbose_name_plural = _("Invitations")
indexes = ExpiringModel.Meta.indexes

View File

@ -104,9 +104,7 @@ class UserWriteStageView(StageView):
for key, value in data.items(): for key, value in data.items():
setter_name = f"set_{key}" setter_name = f"set_{key}"
# Check if user has a setter for this key, like set_password # Check if user has a setter for this key, like set_password
if key == "password": if hasattr(user, setter_name):
user.set_password(value, request=self.request)
elif hasattr(user, setter_name):
setter = getattr(user, setter_name) setter = getattr(user, setter_name)
if callable(setter): if callable(setter):
setter(value) setter(value)

View File

@ -8,11 +8,11 @@ from django.http import HttpResponseNotFound
from django.http.request import urljoin from django.http.request import urljoin
from django.utils.timezone import now from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions
from rest_framework.authentication import get_authorization_header from rest_framework.authentication import get_authorization_header
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField from rest_framework.fields import CharField, IntegerField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import BasePermission
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import DateTimeField, ModelSerializer from rest_framework.serializers import DateTimeField, ModelSerializer
@ -27,7 +27,7 @@ from authentik.recovery.lib import create_admin_group, create_recovery_token
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
class TenantApiKeyPermission(BasePermission): class TenantApiKeyPermission(permissions.BasePermission):
"""Authentication based on tenants.api_key""" """Authentication based on tenants.api_key"""
def has_permission(self, request: Request, view: View) -> bool: def has_permission(self, request: Request, view: View) -> bool:

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2024.12.2 Blueprint schema", "title": "authentik 2024.10.5 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"
@ -3884,7 +3884,8 @@
{ {
"type": "object", "type": "object",
"required": [ "required": [
"model" "model",
"identifiers"
], ],
"properties": { "properties": {
"model": { "model": {
@ -3914,6 +3915,9 @@
}, },
"attrs": { "attrs": {
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint" "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
} }
} }
} }
@ -4159,7 +4163,7 @@
"re_evaluate_policies": { "re_evaluate_policies": {
"type": "boolean", "type": "boolean",
"title": "Re evaluate policies", "title": "Re evaluate policies",
"description": "Evaluate policies when the Stage is presented to the user." "description": "Evaluate policies when the Stage is present to the user."
}, },
"order": { "order": {
"type": "integer", "type": "integer",

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.2} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -54,7 +54,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.2} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

10
go.mod
View File

@ -6,10 +6,10 @@ toolchain go1.23.0
require ( require (
beryju.io/ldap v0.1.0 beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.12.0 github.com/coreos/go-oidc/v3 v3.11.0
github.com/getsentry/sentry-go v0.31.1 github.com/getsentry/sentry-go v0.30.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.9
github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/runtime v0.28.0
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@ -29,9 +29,9 @@ require (
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024122.2 goauthentik.io/api/v3 v3.2024105.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.25.0 golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab layeh.com/radius v0.0.0-20210819152912-ad72663a72ab

25
go.sum
View File

@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@ -86,8 +86,8 @@ github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-ldap/ldap/v3 v3.4.9/go.mod h1:+CE/4PPOOdEPGTi2B7qXKQOq+pNBvXZtlBNcVZY0AWI=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -299,8 +299,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.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2024122.2 h1:QC+ZQ+AxlPwl9OG1X/Z62EVepmTGyfvJUxhUdFjs+4s= goauthentik.io/api/v3 v3.2024105.3 h1:Vl1vwPkCtA8hChsxwO3NUI8nupFC7r93jUHvqM+kYVw=
goauthentik.io/api/v3 v3.2024122.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2024105.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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -312,6 +312,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -385,16 +386,16 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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-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.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -33,7 +33,7 @@ type RedisConfig struct {
Password string `yaml:"password" env:"PASSWORD, overwrite"` Password string `yaml:"password" env:"PASSWORD, overwrite"`
TLS bool `yaml:"tls" env:"TLS, overwrite"` TLS bool `yaml:"tls" env:"TLS, overwrite"`
TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"` TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
TLSCaCert string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"` TLSCaCert *string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"`
} }
type ListenConfig struct { type ListenConfig struct {

View File

@ -16,7 +16,7 @@ func BUILD(def string) string {
func FullVersion() string { func FullVersion() string {
ver := VERSION ver := VERSION
if b := BUILD(""); b != "" { if b := BUILD(""); b != "" {
return fmt.Sprintf("%s+%s", ver, b) ver = fmt.Sprintf("%s.%s", ver, b)
} }
return ver return ver
} }
@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2024.12.2" const VERSION = "2024.10.5"

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"maps"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -17,22 +16,11 @@ import (
"goauthentik.io/internal/constants" "goauthentik.io/internal/constants"
) )
func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string, query url.Values) *url.URL {
wsUrl := &url.URL{}
wsUrl.Scheme = strings.ReplaceAll(akURL.Scheme, "http", "ws")
wsUrl.Host = akURL.Host
_p, _ := url.JoinPath(akURL.Path, "ws/outpost/", outpostUUID, "/")
wsUrl.Path = _p
v := url.Values{}
maps.Insert(v, maps.All(akURL.Query()))
maps.Insert(v, maps.All(query))
wsUrl.RawQuery = v.Encode()
return wsUrl
}
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error { func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
pathTemplate := "%s://%s%sws/outpost/%s/?%s"
query := akURL.Query() query := akURL.Query()
query.Set("instance_uuid", ac.instanceUUID.String()) query.Set("instance_uuid", ac.instanceUUID.String())
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
authHeader := fmt.Sprintf("Bearer %s", ac.token) authHeader := fmt.Sprintf("Bearer %s", ac.token)
@ -49,9 +37,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
}, },
} }
wsu := ac.getWebsocketURL(akURL, outpostUUID, query).String() ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, akURL.Path, outpostUUID, akURL.Query().Encode()), header)
ac.logger.WithField("url", wsu).Debug("connecting to websocket")
ws, _, err := dialer.Dial(wsu, header)
if err != nil { if err != nil {
ac.logger.WithError(err).Warning("failed to connect websocket") ac.logger.WithError(err).Warning("failed to connect websocket")
return err return err

View File

@ -1,42 +0,0 @@
package ak
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func URLMustParse(u string) *url.URL {
ur, err := url.Parse(u)
if err != nil {
panic(err)
}
return ur
}
func TestWebsocketURL(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?foo=bar", nu.String())
}
func TestWebsocketURL_Query(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
v := url.Values{}
v.Set("bar", "baz")
nu := ac.getWebsocketURL(*u, uuid, v)
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?bar=baz&foo=bar", nu.String())
}
func TestWebsocketURL_Subpath(t *testing.T) {
u := URLMustParse("http://localhost:9000/foo/bar/")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
assert.Equal(t, "ws://localhost:9000/foo/bar/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/", nu.String())
}

View File

@ -45,15 +45,15 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
break break
} }
ca := config.Get().Redis.TLSCaCert ca := config.Get().Redis.TLSCaCert
if ca != "" { if ca != nil {
// Get the SystemCertPool, continue with an empty pool on error // Get the SystemCertPool, continue with an empty pool on error
rootCAs, _ := x509.SystemCertPool() rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil { if rootCAs == nil {
rootCAs = x509.NewCertPool() rootCAs = x509.NewCertPool()
} }
certs, err := os.ReadFile(ca) certs, err := os.ReadFile(*ca)
if err != nil { if err != nil {
a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", ca) a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", *ca)
} }
// Append our cert to the system pool // Append our cert to the system pool
if ok := rootCAs.AppendCertsFromPEM(certs); !ok { if ok := rootCAs.AppendCertsFromPEM(certs); !ok {

View File

@ -13,10 +13,6 @@ import (
"goauthentik.io/internal/utils/sentry" "goauthentik.io/internal/utils/sentry"
) )
var (
ErrAuthentikStarting = errors.New("authentik starting")
)
func (ws *WebServer) configureProxy() { func (ws *WebServer) configureProxy() {
// Reverse proxy to the application server // Reverse proxy to the application server
director := func(req *http.Request) { director := func(req *http.Request) {
@ -42,7 +38,7 @@ func (ws *WebServer) configureProxy() {
})) }))
ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) { ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
if !ws.g.IsRunning() { if !ws.g.IsRunning() {
ws.proxyErrorHandler(rw, r, ErrAuthentikStarting) ws.proxyErrorHandler(rw, r, errors.New("authentik starting"))
return return
} }
before := time.Now() before := time.Now()
@ -63,9 +59,7 @@ func (ws *WebServer) configureProxy() {
} }
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
if !errors.Is(err, ErrAuthentikStarting) {
ws.log.WithError(err).Warning("failed to proxy to backend") ws.log.WithError(err).Warning("failed to proxy to backend")
}
rw.WriteHeader(http.StatusBadGateway) rw.WriteHeader(http.StatusBadGateway)
em := fmt.Sprintf("failed to connect to authentik backend: %v", err) em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
// return json if the client asks for json // return json if the client asks for json

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-20 00:08+0000\n" "POT-Creation-Date: 2024-12-18 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -101,10 +101,6 @@ msgstr ""
msgid "Brands" msgid "Brands"
msgstr "" msgstr ""
#: authentik/core/api/application_entitlements.py
msgid "User does not have access to application."
msgstr ""
#: authentik/core/api/devices.py #: authentik/core/api/devices.py
msgid "Extra description not available" msgid "Extra description not available"
msgstr "" msgstr ""
@ -229,14 +225,6 @@ msgstr ""
msgid "Applications" msgid "Applications"
msgstr "" msgstr ""
#: authentik/core/models.py
msgid "Application Entitlement"
msgstr ""
#: authentik/core/models.py
msgid "Application Entitlements"
msgstr ""
#: authentik/core/models.py #: authentik/core/models.py
msgid "Use the source-specific identifier" msgid "Use the source-specific identifier"
msgstr "" msgstr ""

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