diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index e5b75f8ac..a3de32ab1 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -13,7 +13,7 @@ from drf_spectacular.utils import ( inline_serializer, ) from rest_framework.decorators import action -from rest_framework.fields import FileField, SerializerMethodField +from rest_framework.fields import CharField, FileField, SerializerMethodField from rest_framework.parsers import MultiPartParser from rest_framework.request import Request from rest_framework.response import Response @@ -44,6 +44,16 @@ class ApplicationSerializer(ModelSerializer): launch_url = SerializerMethodField() provider_obj = ProviderSerializer(source="get_provider", required=False) + meta_icon = SerializerMethodField() + + def get_meta_icon(self, instance: Application) -> Optional[str]: + """When meta_icon was set to a URL, return the name as-is""" + if not instance.meta_icon: + return None + if instance.meta_icon.name.startswith("http"): + return instance.meta_icon.name + return instance.meta_icon.url + def get_launch_url(self, instance: Application) -> Optional[str]: """Get generated launch URL""" return instance.get_launch_url() @@ -190,6 +200,31 @@ class ApplicationViewSet(ModelViewSet): app.save() return Response({}) + @permission_required("authentik_core.change_application") + @extend_schema( + request=inline_serializer("SetIconURL", fields={"url": CharField()}), + responses={ + 200: OpenApiResponse(description="Success"), + 400: OpenApiResponse(description="Bad request"), + }, + ) + @action( + detail=True, + pagination_class=None, + filter_backends=[], + methods=["POST"], + ) + # pylint: disable=unused-argument + def set_icon_url(self, request: Request, slug: str): + """Set application icon (as URL)""" + app: Application = self.get_object() + url = request.data.get("url", None) + if not url: + return HttpResponseBadRequest() + app.meta_icon = url + app.save() + return Response({}) + @permission_required( "authentik_core.view_application", ["authentik_events.view_event"] ) diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index 1a230388f..269f79173 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -1,5 +1,6 @@ """Flow API Views""" from dataclasses import dataclass +from typing import Optional from django.core.cache import cache from django.db.models import Model @@ -42,6 +43,16 @@ class FlowSerializer(ModelSerializer): cache_count = SerializerMethodField() + background = SerializerMethodField() + + def get_background(self, instance: Flow) -> Optional[str]: + """When background was set to a URL, return the name as-is""" + if not instance.background: + return None + if instance.background.name.startswith("http"): + return instance.background.name + return instance.background.url + def get_cache_count(self, flow: Flow) -> int: """Get count of cached flows""" return len(cache.keys(f"{cache_key(flow)}*")) @@ -284,12 +295,37 @@ class FlowViewSet(ModelViewSet): # pylint: disable=unused-argument def set_background(self, request: Request, slug: str): """Set Flow background""" - app: Flow = self.get_object() + flow: Flow = self.get_object() icon = request.FILES.get("file", None) if not icon: return HttpResponseBadRequest() - app.background = icon - app.save() + flow.background = icon + flow.save() + return Response({}) + + @permission_required("authentik_core.change_application") + @extend_schema( + request=inline_serializer("SetIconURL", fields={"url": CharField()}), + responses={ + 200: OpenApiResponse(description="Success"), + 400: OpenApiResponse(description="Bad request"), + }, + ) + @action( + detail=True, + pagination_class=None, + filter_backends=[], + methods=["POST"], + ) + # pylint: disable=unused-argument + def set_background_url(self, request: Request, slug: str): + """Set Flow background (as URL)""" + flow: Flow = self.get_object() + url = request.data.get("url", None) + if not url: + return HttpResponseBadRequest() + flow.background = url + flow.save() return Response({}) @extend_schema( diff --git a/schema.yml b/schema.yml index 56c8b7997..bc2fdbe67 100644 --- a/schema.yml +++ b/schema.yml @@ -1292,6 +1292,41 @@ paths: description: Bad request '403': $ref: '#/components/schemas/GenericError' + /api/v2beta/core/applications/{slug}/set_icon_url/: + post: + operationId: core_applications_set_icon_url_create + description: Set application icon (as URL) + parameters: + - in: path + name: slug + schema: + type: string + description: Internal application name, used in URLs. + required: true + tags: + - core + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + required: true + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + description: Success + '400': + description: Bad request + '403': + $ref: '#/components/schemas/GenericError' /api/v2beta/core/groups/: get: operationId: core_groups_list @@ -3972,6 +4007,41 @@ paths: description: Bad request '403': $ref: '#/components/schemas/GenericError' + /api/v2beta/flows/instances/{slug}/set_background_url/: + post: + operationId: flows_instances_set_background_url_create + description: Set Flow background (as URL) + parameters: + - in: path + name: slug + schema: + type: string + description: Visible in the URL. + required: true + tags: + - flows + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/SetIconURLRequest' + required: true + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + description: Success + '400': + description: Bad request + '403': + $ref: '#/components/schemas/GenericError' /api/v2beta/flows/instances/cache_clear/: post: operationId: flows_instances_cache_clear_create @@ -23142,6 +23212,13 @@ components: format: binary required: - file + SetIconURLRequest: + type: object + properties: + url: + type: string + required: + - url SeverityEnum: enum: - notice diff --git a/web/src/pages/applications/ApplicationForm.ts b/web/src/pages/applications/ApplicationForm.ts index 9070d20fd..7e2fa0046 100644 --- a/web/src/pages/applications/ApplicationForm.ts +++ b/web/src/pages/applications/ApplicationForm.ts @@ -1,8 +1,8 @@ -import { CoreApi, Application, ProvidersApi, Provider, PolicyEngineMode } from "authentik-api"; +import { CoreApi, Application, ProvidersApi, Provider, PolicyEngineMode, CapabilitiesEnum } from "authentik-api"; import { t } from "@lingui/macro"; import { CSSResult, customElement, property } from "lit-element"; import { html, TemplateResult } from "lit-html"; -import { DEFAULT_CONFIG } from "../../api/Config"; +import { config, DEFAULT_CONFIG } from "../../api/Config"; import { until } from "lit-html/directives/until"; import { ifDefined } from "lit-html/directives/if-defined"; import "../../elements/buttons/Dropdown"; @@ -13,6 +13,7 @@ import "../../elements/forms/HorizontalFormElement"; import "../../elements/forms/FormGroup"; import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; import { ModelForm } from "../../elements/forms/ModelForm"; +import { first } from "../../utils"; @customElement("ak-application-form") export class ApplicationForm extends ModelForm { @@ -50,16 +51,28 @@ export class ApplicationForm extends ModelForm { applicationRequest: data }); } - const icon = this.getFormFile(); - if (icon) { - return writeOp.then(app => { - return new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ - slug: app.slug, - file: icon + return config().then((c) => { + if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) { + const icon = this.getFormFile(); + if (icon) { + return writeOp.then(app => { + return new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ + slug: app.slug, + file: icon + }); + }); + } + } else { + return writeOp.then(app => { + return new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({ + slug: app.slug, + setIconURLRequest: { + url: data.metaIcon || "", + } + }); }); - }); - } - return writeOp; + } + }); }; groupProviders(providers: Provider[]): TemplateResult { @@ -164,11 +177,18 @@ export class ApplicationForm extends ModelForm {

${t`If left empty, authentik will try to extract the launch URL based on the selected provider.`}

- - - + ${until(config().then((c) => { + let type = "text"; + if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) { + type = "file"; + } + return html` + + + `; + }))} diff --git a/web/src/pages/flows/FlowForm.ts b/web/src/pages/flows/FlowForm.ts index 86cd345a8..735999d8c 100644 --- a/web/src/pages/flows/FlowForm.ts +++ b/web/src/pages/flows/FlowForm.ts @@ -1,11 +1,13 @@ -import { Flow, FlowDesignationEnum, PolicyEngineMode, FlowsApi } from "authentik-api"; +import { Flow, FlowDesignationEnum, PolicyEngineMode, FlowsApi, CapabilitiesEnum } from "authentik-api"; import { t } from "@lingui/macro"; import { customElement } from "lit-element"; import { html, TemplateResult } from "lit-html"; -import { DEFAULT_CONFIG } from "../../api/Config"; +import { config, DEFAULT_CONFIG } from "../../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; import "../../elements/forms/HorizontalFormElement"; import { ModelForm } from "../../elements/forms/ModelForm"; +import { until } from "lit-html/directives/until"; +import { first } from "../../utils"; @customElement("ak-flow-form") export class FlowForm extends ModelForm { @@ -36,16 +38,28 @@ export class FlowForm extends ModelForm { flowRequest: data }); } - const background = this.getFormFile(); - if (background) { - return writeOp.then(flow => { - return new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ - slug: flow.slug, - file: background + return config().then((c) => { + if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) { + const icon = this.getFormFile(); + if (icon) { + return writeOp.then(app => { + return new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ + slug: app.slug, + file: icon + }); + }); + } + } else { + return writeOp.then(app => { + return new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundUrlCreate({ + slug: app.slug, + setIconURLRequest: { + url: data.background || "", + } + }); }); - }); - } - return writeOp; + } + }); }; renderDesignations(): TemplateResult { @@ -119,12 +133,19 @@ export class FlowForm extends ModelForm {

${t`Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.`}

- - -

${t`Background shown during execution.`}

-
+ ${until(config().then((c) => { + let type = "text"; + if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) { + type = "file"; + } + return html` + + +

${t`Background shown during execution.`}

+
`; + }))} `; }