From d1fbf2ed65af0b18f5ed32ac189f948e1f32c5fa Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 8 Jan 2025 18:47:35 +0100 Subject: [PATCH 1/4] remove leftover code Signed-off-by: Jens Langhammer --- authentik/core/api/tokens.py | 9 ++---- authentik/providers/oauth2/api/tokens.py | 39 ++---------------------- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index 1b26092905a6..245d1b75cb1c 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -4,7 +4,7 @@ from django.utils.timezone import now 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 from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.fields import CharField @@ -138,13 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet): owner_field = "user" rbac_allow_create_without_perm = True - 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) - def perform_create(self, serializer: TokenSerializer): + # TODO: better permission check if not self.request.user.is_superuser: instance = serializer.save( user=self.request.user, diff --git a/authentik/providers/oauth2/api/tokens.py b/authentik/providers/oauth2/api/tokens.py index 23e3a5dae75c..84680b3a1f09 100644 --- a/authentik/providers/oauth2/api/tokens.py +++ b/authentik/providers/oauth2/api/tokens.py @@ -2,11 +2,8 @@ from json import dumps -from django_filters.rest_framework import DjangoFilterBackend -from guardian.utils import get_anonymous_user from rest_framework import mixins from rest_framework.fields import CharField, ListField, SerializerMethodField -from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.viewsets import GenericViewSet from authentik.core.api.used_by import UsedByMixin @@ -66,17 +63,7 @@ class AuthorizationCodeViewSet( serializer_class = ExpiringBaseGrantModelSerializer filterset_fields = ["user", "provider"] ordering = ["provider", "expires"] - 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) + owner_field = "user" class RefreshTokenViewSet( @@ -92,17 +79,7 @@ class RefreshTokenViewSet( serializer_class = TokenModelSerializer filterset_fields = ["user", "provider"] ordering = ["provider", "expires"] - 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) + owner_field = "user" class AccessTokenViewSet( @@ -118,14 +95,4 @@ class AccessTokenViewSet( serializer_class = TokenModelSerializer filterset_fields = ["user", "provider"] ordering = ["provider", "expires"] - 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) + owner_field = "user" From b7c154ccd265a4129eaa2162ee56425c79381f14 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 8 Jan 2025 18:47:51 +0100 Subject: [PATCH 2/4] dont always try to pull the image Signed-off-by: Jens Langhammer --- authentik/outposts/controllers/docker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/authentik/outposts/controllers/docker.py b/authentik/outposts/controllers/docker.py index 69ea01477f51..28b26f2bee73 100644 --- a/authentik/outposts/controllers/docker.py +++ b/authentik/outposts/controllers/docker.py @@ -13,6 +13,7 @@ from structlog.stdlib import get_logger from yaml import safe_dump +from authentik import __version__ from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException @@ -182,10 +183,16 @@ def try_pull_image(self): `outposts.container_image_base`, but fall back to known-good images""" image = self.get_container_image() try: - self.client.images.pull(image) - except DockerException: # pragma: no cover - image = f"ghcr.io/goauthentik/{self.outpost.type}:latest" - self.client.images.pull(image) + # See if the image exists... + self.client.images.get(image) + except DockerException: + try: + # ...otherwise try to pull it... + self.client.images.pull(image) + except DockerException: + # ...and as a fallback to that default to a sane standard + image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}" + self.client.images.pull(image) return image def _get_container(self) -> tuple[Container, bool]: From 5d28114a4ba2f4e44110f776cef71a1aac5806f7 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 8 Jan 2025 18:48:28 +0100 Subject: [PATCH 3/4] remove USER_ATTRIBUTE_DEBUG Signed-off-by: Jens Langhammer --- authentik/core/api/applications.py | 8 +-- .../migrations/0042_alter_user_options.py | 55 +++++++++++++++++++ authentik/core/models.py | 2 +- .../enterprise/providers/rac/api/endpoints.py | 6 +- authentik/flows/challenge.py | 5 +- authentik/policies/denied.py | 9 ++- blueprints/schema.json | 3 + pyproject.toml | 3 + schema.yml | 16 +++--- .../admin/applications/ApplicationListPage.ts | 2 +- web/src/admin/brands/BrandForm.ts | 2 +- web/src/admin/providers/rac/EndpointList.ts | 2 +- 12 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 authentik/core/migrations/0042_alter_user_options.py diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 54c4e3f3c262..2c2740633069 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -209,7 +209,7 @@ def check_access(self, request: Request, slug: str) -> Response: @extend_schema( parameters=[ OpenApiParameter( - name="superuser_full_list", + name="list_rbac", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, ), @@ -229,10 +229,8 @@ def list(self, request: Request) -> Response: """Custom list method that checks Policy based access instead of guardian""" should_cache = request.query_params.get("search", "") == "" - superuser_full_list = ( - str(request.query_params.get("superuser_full_list", "false")).lower() == "true" - ) - if superuser_full_list and request.user.is_superuser: + list_rbac = str(request.query_params.get("list_rbac", "false")).lower() == "true" + if list_rbac: return super().list(request) only_with_launch_url = str( diff --git a/authentik/core/migrations/0042_alter_user_options.py b/authentik/core/migrations/0042_alter_user_options.py new file mode 100644 index 000000000000..1ccfa2654faa --- /dev/null +++ b/authentik/core/migrations/0042_alter_user_options.py @@ -0,0 +1,55 @@ +# Generated by Django 5.0.10 on 2025-01-08 17:39 + +from django.db import migrations +from django.apps.registry import Apps +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + from django.apps import apps as real_apps + from django.contrib.auth.management import create_permissions + + User = apps.get_model("authentik_core", "User") + USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" + + # Permissions are only created _after_ migrations are run + # - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19 + # - https://stackoverflow.com/a/72029063/1870445 + create_permissions(real_apps.get_app_config("authentik_core"), using=db_alias) + + Permission = apps.get_model("auth", "Permission") + + new_prem = Permission.objects.using(db_alias).get(codename="user_view_debug") + + db_alias = schema_editor.connection.alias + for user in User.objects.using(db_alias).filter( + **{f"attributes__{USER_ATTRIBUTE_DEBUG}": True} + ): + user.permissions.add(new_prem) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_core", "0041_applicationentitlement"), + ] + + operations = [ + migrations.AlterModelOptions( + name="user", + options={ + "permissions": [ + ("reset_user_password", "Reset Password"), + ("impersonate", "Can impersonate other users"), + ("assign_user_permissions", "Can assign permissions to users"), + ("unassign_user_permissions", "Can unassign permissions from users"), + ("preview_user", "Can preview user data sent to providers"), + ("view_user_applications", "View applications the user has access to"), + ("user_view_debug", "User receives additional details for error messages"), + ], + "verbose_name": "User", + "verbose_name_plural": "Users", + }, + ), + migrations.RunPython(migrate_user_debug_attribute), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 1126ab248167..e73e34b24b97 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -41,7 +41,6 @@ from authentik.tenants.utils import get_current_tenant, get_unique_identifier LOGGER = get_logger() -USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated" USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires" USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout" @@ -282,6 +281,7 @@ class Meta: ("unassign_user_permissions", _("Can unassign permissions from users")), ("preview_user", _("Can preview user data sent to providers")), ("view_user_applications", _("View applications the user has access to")), + ("user_view_debug", _("User receives additional details for error messages")), ] indexes = [ models.Index(fields=["last_login"]), diff --git a/authentik/enterprise/providers/rac/api/endpoints.py b/authentik/enterprise/providers/rac/api/endpoints.py index 6cb4aea8fa34..ea8f568cf7cd 100644 --- a/authentik/enterprise/providers/rac/api/endpoints.py +++ b/authentik/enterprise/providers/rac/api/endpoints.py @@ -96,7 +96,7 @@ def _get_allowed_endpoints(self, queryset: QuerySet) -> list[Endpoint]: OpenApiTypes.STR, ), OpenApiParameter( - name="superuser_full_list", + name="list_rbac", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, ), @@ -110,8 +110,8 @@ def list(self, request: Request, *args, **kwargs) -> Response: """List accessible endpoints""" should_cache = request.GET.get("search", "") == "" - superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true" - if superuser_full_list and request.user.is_superuser: + list_rbac = str(request.GET.get("list_rbac", "false")).lower() == "true" + if list_rbac: return super().list(request) queryset = self._filter_queryset_for_list(self.get_queryset()) diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index dfb3585ec4c4..3c5a0f0c8466 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -97,12 +97,9 @@ def __init__(self, request: Request | None = None, error: Exception | None = Non if not request or not error: return self.initial_data["request_id"] = request.request_id - from authentik.core.models import USER_ATTRIBUTE_DEBUG if request.user and request.user.is_authenticated: - if request.user.is_superuser or request.user.group_attributes(request).get( - USER_ATTRIBUTE_DEBUG, False - ): + if request.user.has_perm("authentik_core.user_view_debug"): self.initial_data["error"] = str(error) self.initial_data["traceback"] = exception_to_string(error) diff --git a/authentik/policies/denied.py b/authentik/policies/denied.py index b93348372d83..bd74e055184d 100644 --- a/authentik/policies/denied.py +++ b/authentik/policies/denied.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ -from authentik.core.models import USER_ATTRIBUTE_DEBUG +from authentik.core.models import User from authentik.policies.types import PolicyResult @@ -31,12 +31,11 @@ def resolve_context(self, context: dict[str, Any] | None) -> dict[str, Any] | No if self.error_message: context["error"] = self.error_message # Only show policy result if user is authenticated and - # either superuser or has USER_ATTRIBUTE_DEBUG set + # has permissions to see them if self.policy_result: if self._request.user and self._request.user.is_authenticated: - if self._request.user.is_superuser or self._request.user.group_attributes( - self._request - ).get(USER_ATTRIBUTE_DEBUG, False): + user: User = self._request.user + if user.has_perm("authentik_core.user_view_debug"): context["policy_result"] = self.policy_result context["cancel"] = reverse("authentik_flows:cancel") return context diff --git a/blueprints/schema.json b/blueprints/schema.json index 8fd4002d447e..fee611321393 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -6445,6 +6445,7 @@ "authentik_core.remove_user_from_group", "authentik_core.reset_user_password", "authentik_core.unassign_user_permissions", + "authentik_core.user_view_debug", "authentik_core.view_application", "authentik_core.view_applicationentitlement", "authentik_core.view_authenticatedsession", @@ -12694,6 +12695,7 @@ "authentik_core.remove_user_from_group", "authentik_core.reset_user_password", "authentik_core.unassign_user_permissions", + "authentik_core.user_view_debug", "authentik_core.view_application", "authentik_core.view_applicationentitlement", "authentik_core.view_authenticatedsession", @@ -13202,6 +13204,7 @@ "unassign_user_permissions", "preview_user", "view_user_applications", + "user_view_debug", "add_user", "change_user", "delete_user", diff --git a/pyproject.toml b/pyproject.toml index d1e3acd2c927..0cdabf8d5230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,9 @@ version = "2024.12.2" description = "" authors = ["authentik Team "] +[tool.poetry.requires-plugins] +poetry-plugin-export = ">1.8" + [tool.black] line-length = 100 target-version = ['py312'] diff --git a/schema.yml b/schema.yml index 85d2875976cf..a79c4a3c4d70 100644 --- a/schema.yml +++ b/schema.yml @@ -3391,6 +3391,10 @@ paths: name: group schema: type: string + - in: query + name: list_rbac + schema: + type: boolean - in: query name: meta_description schema: @@ -3439,10 +3443,6 @@ paths: name: slug schema: type: string - - in: query - name: superuser_full_list - schema: - type: boolean tags: - core security: @@ -23204,6 +23204,10 @@ paths: operationId: rac_endpoints_list description: List accessible endpoints parameters: + - in: query + name: list_rbac + schema: + type: boolean - in: query name: name schema: @@ -23234,10 +23238,6 @@ paths: name: search schema: type: string - - in: query - name: superuser_full_list - schema: - type: boolean tags: - rac security: diff --git a/web/src/admin/applications/ApplicationListPage.ts b/web/src/admin/applications/ApplicationListPage.ts index ad18f185ffe7..0bea88bcbe7c 100644 --- a/web/src/admin/applications/ApplicationListPage.ts +++ b/web/src/admin/applications/ApplicationListPage.ts @@ -66,7 +66,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) async apiEndpoint(): Promise> { return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ ...(await this.defaultEndpointConfig()), - superuserFullList: true, + listRbac: true, }); } diff --git a/web/src/admin/brands/BrandForm.ts b/web/src/admin/brands/BrandForm.ts index 6ba00dd6cae7..10440e7162a1 100644 --- a/web/src/admin/brands/BrandForm.ts +++ b/web/src/admin/brands/BrandForm.ts @@ -156,7 +156,7 @@ export class BrandForm extends ModelForm { .fetchObjects=${async (query?: string): Promise => { const args: CoreApplicationsListRequest = { ordering: "name", - superuserFullList: true, + listRbac: true, }; if (query !== undefined) { args.search = query; diff --git a/web/src/admin/providers/rac/EndpointList.ts b/web/src/admin/providers/rac/EndpointList.ts index 06e60477bc16..e24c51ce22fa 100644 --- a/web/src/admin/providers/rac/EndpointList.ts +++ b/web/src/admin/providers/rac/EndpointList.ts @@ -46,7 +46,7 @@ export class EndpointListPage extends Table { return new RacApi(DEFAULT_CONFIG).racEndpointsList({ ...(await this.defaultEndpointConfig()), provider: this.provider?.pk, - superuserFullList: true, + listRbac: true, }); } From 06337283e80fe1c80377272ddc1852aff37918b8 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 16 Jan 2025 15:49:28 +0100 Subject: [PATCH 4/4] fix migrations correctly Signed-off-by: Jens Langhammer --- ...{0042_alter_user_options.py => 0043_alter_user_options.py} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename authentik/core/migrations/{0042_alter_user_options.py => 0043_alter_user_options.py} (93%) diff --git a/authentik/core/migrations/0042_alter_user_options.py b/authentik/core/migrations/0043_alter_user_options.py similarity index 93% rename from authentik/core/migrations/0042_alter_user_options.py rename to authentik/core/migrations/0043_alter_user_options.py index 1ccfa2654faa..dee6b2e55122 100644 --- a/authentik/core/migrations/0042_alter_user_options.py +++ b/authentik/core/migrations/0043_alter_user_options.py @@ -9,6 +9,8 @@ def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEd from django.apps import apps as real_apps from django.contrib.auth.management import create_permissions + db_alias = schema_editor.connection.alias + User = apps.get_model("authentik_core", "User") USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" @@ -31,7 +33,7 @@ def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEd class Migration(migrations.Migration): dependencies = [ - ("authentik_core", "0041_applicationentitlement"), + ("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"), ] operations = [