diff --git a/authentik/core/migrations/0033_alter_user_options.py b/authentik/core/migrations/0033_alter_user_options.py new file mode 100644 index 0000000000..89893251ae --- /dev/null +++ b/authentik/core/migrations/0033_alter_user_options.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.1 on 2024-01-29 12:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_core", "0032_group_roles"), + ] + + 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"), + ], + "verbose_name": "User", + "verbose_name_plural": "Users", + }, + ), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 6c2d5e34fb..b67343270d 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -284,6 +284,8 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): ("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")), ] authentik_signals_ignored_fields = [ # Logged by the events `password_set` diff --git a/authentik/providers/oauth2/api/providers.py b/authentik/providers/oauth2/api/providers.py index 2b03dc4e67..03d88d2904 100644 --- a/authentik/providers/oauth2/api/providers.py +++ b/authentik/providers/oauth2/api/providers.py @@ -1,8 +1,13 @@ """OAuth2Provider API Views""" +from copy import copy + from django.urls import reverse from django.utils import timezone -from drf_spectacular.utils import OpenApiResponse, extend_schema +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from rest_framework.fields import CharField from rest_framework.generics import get_object_or_404 from rest_framework.request import Request @@ -141,23 +146,45 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet): 200: PropertyMappingPreviewSerializer(), 400: OpenApiResponse(description="Bad request"), }, + parameters=[ + OpenApiParameter( + name="for_user", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, + ) + ], ) @action(detail=True, methods=["GET"]) def preview_user(self, request: Request, pk: int) -> Response: """Preview user data for provider""" provider: OAuth2Provider = self.get_object() + for_user = request.user + if "for_user" in request.query_params: + try: + for_user = ( + get_objects_for_user(request.user, "authentik_core.preview_user") + .filter(pk=request.query_params.get("for_user")) + .first() + ) + if not for_user: + raise ValidationError({"for_user": "User not found"}) + except ValueError: + raise ValidationError({"for_user": "input must be numerical"}) + scope_names = ScopeMapping.objects.filter(provider=provider).values_list( "scope_name", flat=True ) + new_request = copy(request._request) + new_request.user = for_user temp_token = IDToken.new( provider, AccessToken( - user=request.user, + user=for_user, provider=provider, _scope=" ".join(scope_names), auth_time=timezone.now(), ), - request, + new_request, ) serializer = PropertyMappingPreviewSerializer(instance={"preview": temp_token.to_dict()}) return Response(serializer.data) diff --git a/authentik/providers/saml/api/providers.py b/authentik/providers/saml/api/providers.py index 226ec7e584..1e2eaa20e8 100644 --- a/authentik/providers/saml/api/providers.py +++ b/authentik/providers/saml/api/providers.py @@ -1,4 +1,5 @@ """SAMLProvider API Views""" +from copy import copy from xml.etree.ElementTree import ParseError # nosec from defusedxml.ElementTree import fromstring @@ -9,6 +10,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import CharField, FileField, SerializerMethodField from rest_framework.parsers import MultiPartParser @@ -277,12 +279,35 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet): 200: PropertyMappingPreviewSerializer(), 400: OpenApiResponse(description="Bad request"), }, + parameters=[ + OpenApiParameter( + name="for_user", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, + ) + ], ) @action(detail=True, methods=["GET"]) def preview_user(self, request: Request, pk: int) -> Response: """Preview user data for provider""" provider: SAMLProvider = self.get_object() - processor = AssertionProcessor(provider, request._request, AuthNRequest()) + for_user = request.user + if "for_user" in request.query_params: + try: + for_user = ( + get_objects_for_user(request.user, "authentik_core.preview_user") + .filter(pk=request.query_params.get("for_user")) + .first() + ) + if not for_user: + raise ValidationError({"for_user": "User not found"}) + except ValueError: + raise ValidationError({"for_user": "input must be numerical"}) + + new_request = copy(request._request) + new_request.user = for_user + + processor = AssertionProcessor(provider, new_request, AuthNRequest()) attributes = processor.get_attributes() name_id = processor.get_name_id() data = [] diff --git a/schema.yml b/schema.yml index 8bbfe4a178..9e3035d67f 100644 --- a/schema.yml +++ b/schema.yml @@ -2931,14 +2931,8 @@ paths: schema: $ref: '#/components/schemas/PolicyTestResult' description: '' - '404': - description: for_user user not found '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' + description: Bad request '403': content: application/json: @@ -16042,6 +16036,10 @@ paths: operationId: providers_oauth2_preview_user_retrieve description: Preview user data for provider parameters: + - in: query + name: for_user + schema: + type: integer - in: path name: id schema: @@ -17409,6 +17407,10 @@ paths: operationId: providers_saml_preview_user_retrieve description: Preview user data for provider parameters: + - in: query + name: for_user + schema: + type: integer - in: path name: id schema: diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts index 3ee1b52d9e..c34f56b61a 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts @@ -1,5 +1,6 @@ import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; +import renderDescriptionList from "@goauthentik/app/components/DescriptionList"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { convertToTitle } from "@goauthentik/common/utils"; @@ -30,11 +31,14 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { + CoreApi, + CoreUsersListRequest, OAuth2Provider, OAuth2ProviderSetupURLs, PropertyMappingPreview, ProvidersApi, RbacPermissionsAssignedByUsersListModelEnum, + User, } from "@goauthentik/api"; @customElement("ak-provider-oauth2-view") @@ -59,6 +63,9 @@ export class OAuth2ProviderViewPage extends AKElement { @state() preview?: PropertyMappingPreview; + @state() + previewUser?: User; + static get styles(): CSSResult[] { return [ PFBase, @@ -83,6 +90,15 @@ export class OAuth2ProviderViewPage extends AKElement { }); } + fetchPreview(): void { + new ProvidersApi(DEFAULT_CONFIG) + .providersOauth2PreviewUserRetrieve({ + id: this.provider?.pk || 0, + forUser: this.previewUser?.pk, + }) + .then((preview) => (this.preview = preview)); + } + render(): TemplateResult { if (!this.provider) { return html``; @@ -107,11 +123,7 @@ export class OAuth2ProviderViewPage extends AKElement { slot="page-preview" data-tab-title="${msg("Preview")}" @activate=${() => { - new ProvidersApi(DEFAULT_CONFIG) - .providersOauth2PreviewUserRetrieve({ - id: this.provider?.pk || 0, - }) - .then((preview) => (this.preview = preview)); + this.fetchPreview(); }} > ${this.renderTabPreview()} @@ -354,8 +366,50 @@ export class OAuth2ProviderViewPage extends AKElement { class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter" >
${value}