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" >
-
- ${msg("Example JWT payload (for currently authenticated user)")} +
${msg("JWT payload")}
+
+ ${renderDescriptionList( + [ + [ + msg("Preview for user"), + html` + => { + const args: CoreUsersListRequest = { + ordering: "username", + }; + if (query !== undefined) { + args.search = query; + } + const users = await new CoreApi( + DEFAULT_CONFIG, + ).coreUsersList(args); + return users.results; + }} + .renderElement=${(user: User): string => { + return user.username; + }} + .renderDescription=${(user: User): TemplateResult => { + return html`${user.name}`; + }} + .value=${(user: User | undefined): number | undefined => { + return user?.pk; + }} + .selected=${(user: User): boolean => { + return user.pk === this.previewUser?.pk; + }} + ?blankable=${true} + @ak-change=${(ev: CustomEvent) => { + this.previewUser = ev.detail.value; + this.fetchPreview(); + }} + > + + `, + ], + ], + { horizontal: true }, + )}
${this.preview diff --git a/web/src/admin/providers/saml/SAMLProviderViewPage.ts b/web/src/admin/providers/saml/SAMLProviderViewPage.ts index de5e3505b1..315a321833 100644 --- a/web/src/admin/providers/saml/SAMLProviderViewPage.ts +++ b/web/src/admin/providers/saml/SAMLProviderViewPage.ts @@ -1,5 +1,6 @@ import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/saml/SAMLProviderForm"; +import renderDescriptionList from "@goauthentik/app/components/DescriptionList"; import "@goauthentik/app/elements/rbac/ObjectPermissionsPage"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; @@ -34,11 +35,14 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { CertificateKeyPair, + CoreApi, + CoreUsersListRequest, CryptoApi, ProvidersApi, RbacPermissionsAssignedByUsersListModelEnum, SAMLMetadata, SAMLProvider, + User, } from "@goauthentik/api"; interface SAMLPreviewAttribute { @@ -96,6 +100,9 @@ export class SAMLProviderViewPage extends AKElement { @state() verifier?: CertificateKeyPair; + @state() + previewUser?: User; + static get styles(): CSSResult[] { return [ PFBase, @@ -120,6 +127,17 @@ export class SAMLProviderViewPage extends AKElement { }); } + fetchPreview(): void { + new ProvidersApi(DEFAULT_CONFIG) + .providersSamlPreviewUserRetrieve({ + id: this.provider?.pk || 0, + forUser: this.previewUser?.pk, + }) + .then((preview) => { + this.preview = preview.preview as SAMLPreviewAttribute; + }); + } + renderRelatedObjects(): TemplateResult { const relatedObjects = []; if (this.provider?.assignedApplicationName) { @@ -203,13 +221,7 @@ export class SAMLProviderViewPage extends AKElement { slot="page-preview" data-tab-title="${msg("Preview")}" @activate=${() => { - new ProvidersApi(DEFAULT_CONFIG) - .providersSamlPreviewUserRetrieve({ - id: this.provider?.pk || 0, - }) - .then((preview) => { - this.preview = preview.preview as SAMLPreviewAttribute; - }); + this.fetchPreview(); }} > ${this.renderTabPreview()} @@ -494,6 +506,47 @@ export class SAMLProviderViewPage extends AKElement { >
${msg("Example SAML attributes")}
+
+ ${renderDescriptionList([ + [ + "Preview for user", + html` + => { + const args: CoreUsersListRequest = { + ordering: "username", + }; + if (query !== undefined) { + args.search = query; + } + const users = await new CoreApi( + DEFAULT_CONFIG, + ).coreUsersList(args); + return users.results; + }} + .renderElement=${(user: User): string => { + return user.username; + }} + .renderDescription=${(user: User): TemplateResult => { + return html`${user.name}`; + }} + .value=${(user: User | undefined): number | undefined => { + return user?.pk; + }} + .selected=${(user: User): boolean => { + return user.pk === this.previewUser?.pk; + }} + ?blankable=${true} + @ak-change=${(ev: CustomEvent) => { + this.previewUser = ev.detail.value; + this.fetchPreview(); + }} + > + + `, + ], + ])} +
@@ -519,11 +572,7 @@ export class SAMLProviderViewPage extends AKElement {
-
    - ${attr.Value.map((value) => { - return html`
  • ${value}
  • `; - })} -
+
    `; diff --git a/web/src/components/DescriptionList.ts b/web/src/components/DescriptionList.ts index cfeee4640f..efac7e6e28 100644 --- a/web/src/components/DescriptionList.ts +++ b/web/src/components/DescriptionList.ts @@ -1,3 +1,5 @@ +import { first } from "@goauthentik/app/common/utils"; + import { TemplateResult, html, nothing } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { map } from "lit/directives/map.js"; @@ -7,10 +9,10 @@ export type DescriptionPair = [string, DescriptionDesc]; export type DescriptionRecord = { term: string; desc: DescriptionDesc }; interface DescriptionConfig { - horizontal: boolean; - compact: boolean; - twocolumn: boolean; - threecolumn: boolean; + horizontal?: boolean; + compact?: boolean; + twocolumn?: boolean; + threecolumn?: boolean; } const isDescriptionRecordCollection = (v: Array): v is DescriptionRecord[] => @@ -78,10 +80,10 @@ export function renderDescriptionList( ) { const checkedTerms = alignTermType(terms); const classes = classMap({ - "pf-m-horizontal": config.horizontal, - "pf-m-compact": config.compact, - "pf-m-2-col-on-lg": config.twocolumn, - "pf-m-3-col-on-lg": config.threecolumn, + "pf-m-horizontal": first(config.horizontal, false), + "pf-m-compact": first(config.compact, false), + "pf-m-2-col-on-lg": first(config.twocolumn, false), + "pf-m-3-col-on-lg": first(config.threecolumn, false), }); return html` diff --git a/web/src/elements/charts/Chart.ts b/web/src/elements/charts/Chart.ts index 984d17696a..c5eaa74562 100644 --- a/web/src/elements/charts/Chart.ts +++ b/web/src/elements/charts/Chart.ts @@ -168,6 +168,7 @@ export abstract class AKChart extends AKElement { getOptions(): ChartOptions { return { maintainAspectRatio: false, + responsive: true, scales: { x: { type: "time", diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf index 6f4d9f29a0..0a0186eba3 100644 --- a/web/xliff/de.xlf +++ b/web/xliff/de.xlf @@ -1544,9 +1544,6 @@ JWKS URL JWKS URL - - Example JWT payload (for currently authenticated user) - Forward auth (domain-level) Authentifizierung weiterleiten (Domänenebene) @@ -6387,6 +6384,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/en.xlf b/web/xliff/en.xlf index 692e5ff271..12ac5aa493 100644 --- a/web/xliff/en.xlf +++ b/web/xliff/en.xlf @@ -1617,10 +1617,6 @@ JWKS URL JWKS URL - - Example JWT payload (for currently authenticated user) - Example JWT payload (for currently authenticated user) - Forward auth (domain-level) Forward auth (domain-level) @@ -6658,6 +6654,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/es.xlf b/web/xliff/es.xlf index bbdccb38da..81993ba417 100644 --- a/web/xliff/es.xlf +++ b/web/xliff/es.xlf @@ -1516,9 +1516,6 @@ JWKS URL - - Example JWT payload (for currently authenticated user) - Forward auth (domain-level) Autenticación directa (nivel de dominio) @@ -6303,6 +6300,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index dcd40e9297..3c6dcfe62a 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -2014,11 +2014,6 @@ JWKS URL URL JWKS - - - Example JWT payload (for currently authenticated user) - Exemple de charge utile JWT (pour l'utilisateur actuellement authentifié) - Forward auth (domain-level) @@ -8383,6 +8378,12 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/ko.xlf b/web/xliff/ko.xlf index b9ef7b21ca..83a4124bfe 100644 --- a/web/xliff/ko.xlf +++ b/web/xliff/ko.xlf @@ -2008,11 +2008,6 @@ JWKS URL JWKS URL - - - Example JWT payload (for currently authenticated user) - JWT 페이로드 예시(현재 인가된 사용자의 경우) - Forward auth (domain-level) @@ -8253,6 +8248,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/nl.xlf b/web/xliff/nl.xlf index 2f86986d4a..e46e7f430a 100644 --- a/web/xliff/nl.xlf +++ b/web/xliff/nl.xlf @@ -1996,11 +1996,6 @@ JWKS URL JWKS-URL - - - Example JWT payload (for currently authenticated user) - Voorbeeld JWT-payload (voor momenteel geauthenticeerde gebruiker) - Forward auth (domain-level) @@ -8096,6 +8091,12 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/pl.xlf b/web/xliff/pl.xlf index 652eff4c4b..eb0b2a8cf1 100644 --- a/web/xliff/pl.xlf +++ b/web/xliff/pl.xlf @@ -1561,10 +1561,6 @@ JWKS URL URL JWKS - - Example JWT payload (for currently authenticated user) - Przykładowy ładunek JWT (dla aktualnie uwierzytelnionego użytkownika) - Forward auth (domain-level) Forward auth (na poziomie domeny) @@ -6510,6 +6506,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/pseudo-LOCALE.xlf b/web/xliff/pseudo-LOCALE.xlf index d49470c14d..bec6613ec8 100644 --- a/web/xliff/pseudo-LOCALE.xlf +++ b/web/xliff/pseudo-LOCALE.xlf @@ -1997,11 +1997,6 @@ JWKS URL ĵŴĶŚ ŨŔĹ - - - Example JWT payload (for currently authenticated user) - Ēxàmƥĺē ĵŴŢ ƥàŷĺōàď (ƒōŕ ćũŕŕēńţĺŷ àũţĥēńţĩćàţēď ũśēŕ) - Forward auth (domain-level) @@ -8230,4 +8225,10 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + JWT payload + + + Preview for user + diff --git a/web/xliff/tr.xlf b/web/xliff/tr.xlf index 5c4b77f308..b306b787ce 100644 --- a/web/xliff/tr.xlf +++ b/web/xliff/tr.xlf @@ -1515,9 +1515,6 @@ JWKS URL - - Example JWT payload (for currently authenticated user) - Forward auth (domain-level) İleri kimlik doğrulama (alan düzeyi) @@ -6296,6 +6293,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/zh-CN.xlf b/web/xliff/zh-CN.xlf index be9998976e..d75c53648a 100644 --- a/web/xliff/zh-CN.xlf +++ b/web/xliff/zh-CN.xlf @@ -1483,9 +1483,6 @@ JWKS URL - - Example JWT payload (for currently authenticated user) - Yes @@ -5206,6 +5203,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + JWT payload + + + Preview for user + diff --git a/web/xliff/zh-Hans.xlf b/web/xliff/zh-Hans.xlf index 5f5e2c6ab6..b5f58fe26b 100644 --- a/web/xliff/zh-Hans.xlf +++ b/web/xliff/zh-Hans.xlf @@ -2015,11 +2015,6 @@ JWKS URL JWKS URL - - - Example JWT payload (for currently authenticated user) - 示例 JWT 载荷(当前经过身份验证的用户) - Forward auth (domain-level) @@ -8366,10 +8361,6 @@ Bindings to groups/users are checked against the user of the event. Selected Applications 已选应用 - - This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows: - 此选项配置流程执行器页面上的页脚链接。必须为有效的 YAML 或 JSON 列表,可以使用以下值: - This feature requires an enterprise license. @@ -8408,6 +8399,16 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows: + 此选项配置流程执行器页面上的页脚链接。必须为有效的 YAML 或 JSON 列表,可以使用以下值: + + + JWT payload + + + Preview for user diff --git a/web/xliff/zh-Hant.xlf b/web/xliff/zh-Hant.xlf index 0ff8d6376e..67eb41ef69 100644 --- a/web/xliff/zh-Hant.xlf +++ b/web/xliff/zh-Hant.xlf @@ -1529,9 +1529,6 @@ JWKS URL - - Example JWT payload (for currently authenticated user) - Forward auth (domain-level) 转发身份验证(域级) @@ -6344,6 +6341,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user diff --git a/web/xliff/zh_TW.xlf b/web/xliff/zh_TW.xlf index df46c9afff..4b970d85ec 100644 --- a/web/xliff/zh_TW.xlf +++ b/web/xliff/zh_TW.xlf @@ -1998,11 +1998,6 @@ JWKS URL JWKS 網址 - - - Example JWT payload (for currently authenticated user) - 範例 JWT 酬載(給目前已認證的使用者) - Forward auth (domain-level) @@ -8214,6 +8209,12 @@ Bindings to groups/users are checked against the user of the event. Permissions assigned to this role which affect all object instances of a given type. + + + JWT payload + + + Preview for user