From 9f5fb692ba88df0b9d3350fedad19da4a8303e47 Mon Sep 17 00:00:00 2001
From: Jens L
Date: Wed, 16 Nov 2022 14:10:10 +0100
Subject: [PATCH] sources: add custom icon support (#4022)
* add source icon
Signed-off-by: Jens Langhammer
* add to oauth form
Signed-off-by: Jens Langhammer
* add to other browser sources
Signed-off-by: Jens Langhammer
* add migration, return icon in UI challenges
Signed-off-by: Jens Langhammer
* deduplicate file upload
Signed-off-by: Jens Langhammer
Signed-off-by: Jens Langhammer
---
authentik/core/api/applications.py | 30 ++---
authentik/core/api/sources.py | 55 ++++++++-
authentik/core/api/utils.py | 15 +--
authentik/core/migrations/0024_source_icon.py | 20 +++
authentik/core/models.py | 16 +++
authentik/flows/api/flows.py | 42 ++-----
authentik/lib/utils/file.py | 55 +++++++++
authentik/sources/oauth/api/source.py | 3 +-
authentik/sources/oauth/models.py | 11 +-
authentik/sources/plex/models.py | 10 +-
authentik/sources/saml/api/source.py | 22 +++-
authentik/sources/saml/models.py | 6 +-
schema.yml | 115 ++++++++++++++----
.../admin/sources/oauth/OAuthSourceForm.ts | 84 ++++++++++++-
web/src/admin/sources/plex/PlexSourceForm.ts | 84 ++++++++++++-
web/src/admin/sources/saml/SAMLSourceForm.ts | 82 ++++++++++++-
16 files changed, 527 insertions(+), 123 deletions(-)
create mode 100644 authentik/core/migrations/0024_source_icon.py
create mode 100644 authentik/lib/utils/file.py
diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py
index 632e7243e..4a52c847e 100644
--- a/authentik/core/api/applications.py
+++ b/authentik/core/api/applications.py
@@ -23,10 +23,15 @@ from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
-from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
from authentik.core.models import Application, User
from authentik.events.models import EventAction
from authentik.events.utils import sanitize_dict
+from authentik.lib.utils.file import (
+ FilePathSerializer,
+ FileUploadSerializer,
+ set_file,
+ set_file_url,
+)
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
@@ -224,21 +229,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def set_icon(self, request: Request, slug: str):
"""Set application icon"""
app: Application = self.get_object()
- icon = request.FILES.get("file", None)
- clear = request.data.get("clear", "false").lower() == "true"
- if clear:
- # .delete() saves the model by default
- app.meta_icon.delete()
- return Response({})
- if icon:
- app.meta_icon = icon
- try:
- app.save()
- except PermissionError as exc:
- LOGGER.warning("Failed to save icon", exc=exc)
- return HttpResponseBadRequest()
- return Response({})
- return HttpResponseBadRequest()
+ return set_file(request, app, "meta_icon")
@permission_required("authentik_core.change_application")
@extend_schema(
@@ -258,12 +249,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
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 url is None:
- return HttpResponseBadRequest()
- app.meta_icon.name = url
- app.save()
- return Response({})
+ return set_file_url(request, app, "meta_icon")
@permission_required("authentik_core.view_application", ["authentik_events.view_event"])
@extend_schema(responses={200: CoordinateSerializer(many=True)})
diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py
index 209a3eda5..899a2ab93 100644
--- a/authentik/core/api/sources.py
+++ b/authentik/core/api/sources.py
@@ -2,10 +2,11 @@
from typing import Iterable
from django_filters.rest_framework import DjangoFilterBackend
-from drf_spectacular.utils import extend_schema
+from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ReadOnlyField, SerializerMethodField
@@ -13,10 +14,17 @@ from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
+from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
+from authentik.lib.utils.file import (
+ FilePathSerializer,
+ FileUploadSerializer,
+ set_file,
+ set_file_url,
+)
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
@@ -28,6 +36,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
managed = ReadOnlyField()
component = SerializerMethodField()
+ icon = ReadOnlyField(source="get_icon")
def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object"""
@@ -54,6 +63,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"user_matching_mode",
"managed",
"user_path_template",
+ "icon",
]
@@ -75,6 +85,49 @@ class SourceViewSet(
def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses()
+ @permission_required("authentik_core.change_source")
+ @extend_schema(
+ request={
+ "multipart/form-data": FileUploadSerializer,
+ },
+ responses={
+ 200: OpenApiResponse(description="Success"),
+ 400: OpenApiResponse(description="Bad request"),
+ },
+ )
+ @action(
+ detail=True,
+ pagination_class=None,
+ filter_backends=[],
+ methods=["POST"],
+ parser_classes=(MultiPartParser,),
+ )
+ # pylint: disable=unused-argument
+ def set_icon(self, request: Request, slug: str):
+ """Set source icon"""
+ source: Source = self.get_object()
+ return set_file(request, source, "icon")
+
+ @permission_required("authentik_core.change_source")
+ @extend_schema(
+ request=FilePathSerializer,
+ 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 source icon (as URL)"""
+ source: Source = self.get_object()
+ return set_file_url(request, source, "icon")
+
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py
index 6c28debd6..40c9e9e1d 100644
--- a/authentik/core/api/utils.py
+++ b/authentik/core/api/utils.py
@@ -2,7 +2,7 @@
from typing import Any
from django.db.models import Model
-from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
+from rest_framework.fields import CharField, IntegerField
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
@@ -23,19 +23,6 @@ class PassiveSerializer(Serializer):
return Model()
-class FileUploadSerializer(PassiveSerializer):
- """Serializer to upload file"""
-
- file = FileField(required=False)
- clear = BooleanField(default=False)
-
-
-class FilePathSerializer(PassiveSerializer):
- """Serializer to upload file"""
-
- url = CharField()
-
-
class MetaNameSerializer(PassiveSerializer):
"""Add verbose names to response"""
diff --git a/authentik/core/migrations/0024_source_icon.py b/authentik/core/migrations/0024_source_icon.py
new file mode 100644
index 000000000..79347b67e
--- /dev/null
+++ b/authentik/core/migrations/0024_source_icon.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.1.3 on 2022-11-15 20:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_core", "0023_source_authentik_c_slug_ccb2e5_idx_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="source",
+ name="icon",
+ field=models.FileField(
+ default=None, max_length=500, null=True, upload_to="source-icons/"
+ ),
+ ),
+ ]
diff --git a/authentik/core/models.py b/authentik/core/models.py
index 6946adb39..687492be1 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -421,6 +421,12 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
+ icon = models.FileField(
+ upload_to="source-icons/",
+ default=None,
+ null=True,
+ max_length=500,
+ )
authentication_flow = models.ForeignKey(
"authentik_flows.Flow",
@@ -454,6 +460,16 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
objects = InheritanceManager()
+ @property
+ def get_icon(self) -> Optional[str]:
+ """Get the URL to the Icon. If the name is /static or
+ starts with http it is returned as-is"""
+ if not self.icon:
+ return None
+ if "://" in self.icon.name or self.icon.name.startswith("/static"):
+ return self.icon.name
+ return self.icon.url
+
def get_user_path(self) -> str:
"""Get user path, fallback to default for formatting errors"""
try:
diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py
index 8ab17010d..670128c26 100644
--- a/authentik/flows/api/flows.py
+++ b/authentik/flows/api/flows.py
@@ -1,7 +1,6 @@
"""Flow API Views"""
from django.core.cache import cache
from django.http import HttpResponse
-from django.http.response import HttpResponseBadRequest
from django.urls import reverse
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
@@ -19,19 +18,19 @@ from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer
from authentik.core.api.used_by import UsedByMixin
-from authentik.core.api.utils import (
- CacheSerializer,
- FilePathSerializer,
- FileUploadSerializer,
- LinkSerializer,
- PassiveSerializer,
-)
+from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.utils import sanitize_dict
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
+from authentik.lib.utils.file import (
+ FilePathSerializer,
+ FileUploadSerializer,
+ set_file,
+ set_file_url,
+)
from authentik.lib.views import bad_request_message
LOGGER = get_logger()
@@ -249,25 +248,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def set_background(self, request: Request, slug: str):
"""Set Flow background"""
flow: Flow = self.get_object()
- background = request.FILES.get("file", None)
- clear = request.data.get("clear", "false").lower() == "true"
- if clear:
- if flow.background_url.startswith("/media"):
- # .delete() saves the model by default
- flow.background.delete()
- else:
- flow.background = None
- flow.save()
- return Response({})
- if background:
- flow.background = background
- try:
- flow.save()
- except PermissionError as exc:
- LOGGER.warning("Failed to save icon", exc=exc)
- return HttpResponseBadRequest()
- return Response({})
- return HttpResponseBadRequest()
+ return set_file(request, flow, "background")
@permission_required("authentik_core.change_application")
@extend_schema(
@@ -287,12 +268,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
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.name = url
- flow.save()
- return Response({})
+ return set_file_url(request, flow, "background")
@extend_schema(
responses={
diff --git a/authentik/lib/utils/file.py b/authentik/lib/utils/file.py
new file mode 100644
index 000000000..52e24212b
--- /dev/null
+++ b/authentik/lib/utils/file.py
@@ -0,0 +1,55 @@
+"""file utils"""
+from django.db.models import Model
+from django.http import HttpResponseBadRequest
+from rest_framework.fields import BooleanField, CharField, FileField
+from rest_framework.request import Request
+from rest_framework.response import Response
+from structlog import get_logger
+
+from authentik.core.api.utils import PassiveSerializer
+
+LOGGER = get_logger()
+
+
+class FileUploadSerializer(PassiveSerializer):
+ """Serializer to upload file"""
+
+ file = FileField(required=False)
+ clear = BooleanField(default=False)
+
+
+class FilePathSerializer(PassiveSerializer):
+ """Serializer to upload file"""
+
+ url = CharField()
+
+
+def set_file(request: Request, obj: Model, field: str):
+ """Upload file"""
+ field = getattr(obj, field)
+ icon = request.FILES.get("file", None)
+ clear = request.data.get("clear", "false").lower() == "true"
+ if clear:
+ # .delete() saves the model by default
+ field.delete()
+ return Response({})
+ if icon:
+ field = icon
+ try:
+ obj.save()
+ except PermissionError as exc:
+ LOGGER.warning("Failed to save file", exc=exc)
+ return HttpResponseBadRequest()
+ return Response({})
+ return HttpResponseBadRequest()
+
+
+def set_file_url(request: Request, obj: Model, field: str):
+ """Set file field to URL"""
+ field = getattr(obj, field)
+ url = request.data.get("url", None)
+ if url is None:
+ return HttpResponseBadRequest()
+ field.name = url
+ obj.save()
+ return Response({})
diff --git a/authentik/sources/oauth/api/source.py b/authentik/sources/oauth/api/source.py
index f35aa46ab..9dce61578 100644
--- a/authentik/sources/oauth/api/source.py
+++ b/authentik/sources/oauth/api/source.py
@@ -35,6 +35,7 @@ class OAuthSourceSerializer(SourceSerializer):
provider_type = ChoiceField(choices=registry.get_name_tuple())
callback_url = SerializerMethodField()
+ type = SerializerMethodField()
def get_callback_url(self, instance: OAuthSource) -> str:
"""Get OAuth Callback URL"""
@@ -46,8 +47,6 @@ class OAuthSourceSerializer(SourceSerializer):
return relative_url
return self.context["request"].build_absolute_uri(relative_url)
- type = SerializerMethodField()
-
@extend_schema_field(SourceTypeSerializer)
def get_type(self, instance: OAuthSource) -> SourceTypeSerializer:
"""Get source's type configuration"""
diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py
index 8b2bf90c1..8e559878e 100644
--- a/authentik/sources/oauth/models.py
+++ b/authentik/sources/oauth/models.py
@@ -75,15 +75,20 @@ class OAuthSource(Source):
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.type
provider = provider_type()
+ icon = self.get_icon
+ if not icon:
+ icon = provider.icon_url()
return UILoginButton(
name=self.name,
- icon_url=provider.icon_url(),
challenge=provider.login_challenge(self, request),
+ icon_url=icon,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
provider_type = self.type
- provider = provider_type()
+ icon = self.get_icon
+ if not icon:
+ icon = provider_type().icon_url()
return UserSettingSerializer(
data={
"title": self.name,
@@ -92,7 +97,7 @@ class OAuthSource(Source):
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
- "icon_url": provider.icon_url(),
+ "icon_url": icon,
}
)
diff --git a/authentik/sources/plex/models.py b/authentik/sources/plex/models.py
index 19d232824..01b3bdfe8 100644
--- a/authentik/sources/plex/models.py
+++ b/authentik/sources/plex/models.py
@@ -64,6 +64,9 @@ class PlexSource(Source):
return PlexSourceSerializer
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
+ icon = self.get_icon
+ if not icon:
+ icon = static("authentik/sources/plex.svg")
return UILoginButton(
challenge=PlexAuthenticationChallenge(
{
@@ -73,17 +76,20 @@ class PlexSource(Source):
"slug": self.slug,
}
),
- icon_url=static("authentik/sources/plex.svg"),
+ icon_url=icon,
name=self.name,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
+ icon = self.get_icon
+ if not icon:
+ icon = static("authentik/sources/plex.svg")
return UserSettingSerializer(
data={
"title": self.name,
"component": "ak-user-settings-source-plex",
"configure_url": self.client_id,
- "icon_url": static("authentik/sources/plex.svg"),
+ "icon_url": icon,
}
)
diff --git a/authentik/sources/saml/api/source.py b/authentik/sources/saml/api/source.py
index 618dc0311..4a70b42eb 100644
--- a/authentik/sources/saml/api/source.py
+++ b/authentik/sources/saml/api/source.py
@@ -40,7 +40,27 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
queryset = SAMLSource.objects.all()
serializer_class = SAMLSourceSerializer
lookup_field = "slug"
- filterset_fields = "__all__"
+ filterset_fields = [
+ "name",
+ "slug",
+ "enabled",
+ "authentication_flow",
+ "enrollment_flow",
+ "managed",
+ "policy_engine_mode",
+ "user_matching_mode",
+ "pre_authentication_flow",
+ "issuer",
+ "sso_url",
+ "slo_url",
+ "allow_idp_initiated",
+ "name_id_policy",
+ "binding_type",
+ "signing_kp",
+ "digest_algorithm",
+ "signature_algorithm",
+ "temporary_user_delete_after",
+ ]
search_fields = ["name", "slug"]
ordering = ["name"]
diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py
index 8dd30cc34..c1a3e028e 100644
--- a/authentik/sources/saml/models.py
+++ b/authentik/sources/saml/models.py
@@ -191,9 +191,13 @@ class SAMLSource(Source):
}
),
name=self.name,
+ icon_url=self.get_icon,
)
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
+ icon = self.get_icon
+ if not icon:
+ icon = static(f"authentik/sources/{self.slug}.svg")
return UserSettingSerializer(
data={
"title": self.name,
@@ -202,7 +206,7 @@ class SAMLSource(Source):
"authentik_sources_saml:login",
kwargs={"source_slug": self.slug},
),
- "icon_url": static(f"authentik/sources/{self.slug}.svg"),
+ "icon_url": icon,
}
)
diff --git a/schema.yml b/schema.yml
index c8e3886e9..a3a994254 100644
--- a/schema.yml
+++ b/schema.yml
@@ -15795,6 +15795,69 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
+ /sources/all/{slug}/set_icon/:
+ post:
+ operationId: sources_all_set_icon_create
+ description: Set source icon
+ parameters:
+ - in: path
+ name: slug
+ schema:
+ type: string
+ description: Internal source name, used in URLs.
+ required: true
+ tags:
+ - sources
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/FileUploadRequest'
+ security:
+ - authentik: []
+ responses:
+ '200':
+ description: Success
+ '400':
+ description: Bad request
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
+ /sources/all/{slug}/set_icon_url/:
+ post:
+ operationId: sources_all_set_icon_url_create
+ description: Set source icon (as URL)
+ parameters:
+ - in: path
+ name: slug
+ schema:
+ type: string
+ description: Internal source name, used in URLs.
+ required: true
+ tags:
+ - sources
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FilePathRequest'
+ required: true
+ security:
+ - authentik: []
+ responses:
+ '200':
+ description: Success
+ '400':
+ description: Bad request
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericError'
+ description: ''
/sources/all/{slug}/used_by/:
get:
operationId: sources_all_used_by_list
@@ -17092,20 +17155,6 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- - in: query
- name: pbm_uuid
- schema:
- type: string
- format: uuid
- - in: query
- name: policies
- schema:
- type: array
- items:
- type: string
- format: uuid
- explode: true
- style: form
- in: query
name: policy_engine_mode
schema:
@@ -17118,15 +17167,6 @@ paths:
schema:
type: string
format: uuid
- - in: query
- name: property_mappings
- schema:
- type: array
- items:
- type: string
- format: uuid
- explode: true
- style: form
- name: search
required: false
in: query
@@ -17176,10 +17216,6 @@ paths:
- username_link
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
- - in: query
- name: user_path_template
- schema:
- type: string
tags:
- sources
security:
@@ -28871,6 +28907,10 @@ components:
readOnly: true
user_path_template:
type: string
+ icon:
+ type: string
+ nullable: true
+ readOnly: true
server_uri:
type: string
format: uri
@@ -28933,6 +28973,7 @@ components:
required:
- base_dn
- component
+ - icon
- managed
- meta_model_name
- name
@@ -29662,6 +29703,10 @@ components:
readOnly: true
user_path_template:
type: string
+ icon:
+ type: string
+ nullable: true
+ readOnly: true
provider_type:
$ref: '#/components/schemas/ProviderTypeEnum'
request_token_url:
@@ -29707,6 +29752,7 @@ components:
- callback_url
- component
- consumer_key
+ - icon
- managed
- meta_model_name
- name
@@ -35075,6 +35121,10 @@ components:
readOnly: true
user_path_template:
type: string
+ icon:
+ type: string
+ nullable: true
+ readOnly: true
client_id:
type: string
description: Client identifier used to talk to Plex.
@@ -35092,6 +35142,7 @@ components:
description: Plex token used to check friends
required:
- component
+ - icon
- managed
- meta_model_name
- name
@@ -36495,6 +36546,10 @@ components:
readOnly: true
user_path_template:
type: string
+ icon:
+ type: string
+ nullable: true
+ readOnly: true
pre_authentication_flow:
type: string
format: uuid
@@ -36543,6 +36598,7 @@ components:
doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
required:
- component
+ - icon
- managed
- meta_model_name
- name
@@ -36936,8 +36992,13 @@ components:
readOnly: true
user_path_template:
type: string
+ icon:
+ type: string
+ nullable: true
+ readOnly: true
required:
- component
+ - icon
- managed
- meta_model_name
- name
diff --git a/web/src/admin/sources/oauth/OAuthSourceForm.ts b/web/src/admin/sources/oauth/OAuthSourceForm.ts
index 0cad08b7b..ae12a151b 100644
--- a/web/src/admin/sources/oauth/OAuthSourceForm.ts
+++ b/web/src/admin/sources/oauth/OAuthSourceForm.ts
@@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
-import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
@@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
+ CapabilitiesEnum,
FlowsApi,
FlowsInstancesListDesignationEnum,
OAuthSource,
@@ -57,6 +58,9 @@ export class OAuthSourceForm extends ModelForm {
@property({ attribute: false })
providerType: SourceType | null = null;
+ @state()
+ clearIcon = false;
+
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated source.`;
@@ -65,18 +69,38 @@ export class OAuthSourceForm extends ModelForm {
}
}
- send = (data: OAuthSource): Promise => {
+ send = async (data: OAuthSource): Promise => {
data.providerType = (this.providerType?.slug || "") as ProviderTypeEnum;
- if (this.instance?.slug) {
- return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
+ let source: OAuthSource;
+ if (this.instance) {
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
slug: this.instance.slug,
patchedOAuthSourceRequest: data,
});
} else {
- return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
});
}
+ const c = await config();
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ const icon = this.getFormFiles()["icon"];
+ if (icon || this.clearIcon) {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
+ slug: source.slug,
+ file: icon,
+ clear: this.clearIcon,
+ });
+ }
+ } else {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
+ slug: source.slug,
+ filePathRequest: {
+ url: data.icon || "",
+ },
+ });
+ }
+ return source;
};
renderUrlOptions(): TemplateResult {
@@ -282,6 +306,54 @@ export class OAuthSourceForm extends ModelForm {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
+ ${until(
+ config().then((c) => {
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ return html`
+
+ ${this.instance?.icon
+ ? html`
+
+ ${t`Currently set to:`} ${this.instance?.icon}
+
+ `
+ : html``}
+
+ ${this.instance?.icon
+ ? html`
+
+
+ {
+ const target = ev.target as HTMLInputElement;
+ this.clearIcon = target.checked;
+ }}
+ />
+
+
+
+ ${t`Delete currently set icon.`}
+
+
+ `
+ : html``}`;
+ }
+ return html`
+
+
+ ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
+
+ `;
+ }),
+ )}
${t`Protocol settings`}
diff --git a/web/src/admin/sources/plex/PlexSourceForm.ts b/web/src/admin/sources/plex/PlexSourceForm.ts
index 9e1e35249..90b1acb51 100644
--- a/web/src/admin/sources/plex/PlexSourceForm.ts
+++ b/web/src/admin/sources/plex/PlexSourceForm.ts
@@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
-import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
@@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
+ CapabilitiesEnum,
FlowsApi,
FlowsInstancesListDesignationEnum,
PlexSource,
@@ -35,6 +36,9 @@ export class PlexSourceForm extends ModelForm {
});
}
+ @state()
+ clearIcon = false;
+
@property()
plexToken?: string;
@@ -55,18 +59,38 @@ export class PlexSourceForm extends ModelForm {
}
}
- send = (data: PlexSource): Promise => {
+ send = async (data: PlexSource): Promise => {
data.plexToken = this.plexToken || "";
- if (this.instance?.slug) {
- return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
+ let source: PlexSource;
+ if (this.instance) {
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
slug: this.instance.slug,
plexSourceRequest: data,
});
} else {
- return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
plexSourceRequest: data,
});
}
+ const c = await config();
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ const icon = this.getFormFiles()["icon"];
+ if (icon || this.clearIcon) {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
+ slug: source.slug,
+ file: icon,
+ clear: this.clearIcon,
+ });
+ }
+ } else {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
+ slug: source.slug,
+ filePathRequest: {
+ url: data.icon || "",
+ },
+ });
+ }
+ return source;
};
async doAuth(): Promise {
@@ -229,6 +253,54 @@ export class PlexSourceForm extends ModelForm {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
+ ${until(
+ config().then((c) => {
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ return html`
+
+ ${this.instance?.icon
+ ? html`
+
+ ${t`Currently set to:`} ${this.instance?.icon}
+
+ `
+ : html``}
+
+ ${this.instance?.icon
+ ? html`
+
+
+ {
+ const target = ev.target as HTMLInputElement;
+ this.clearIcon = target.checked;
+ }}
+ />
+
+
+
+ ${t`Delete currently set icon.`}
+
+
+ `
+ : html``}`;
+ }
+ return html`
+
+
+ ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
+
+ `;
+ }),
+ )}
${t`Protocol settings`}
diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts
index d760951aa..21d422efb 100644
--- a/web/src/admin/sources/saml/SAMLSourceForm.ts
+++ b/web/src/admin/sources/saml/SAMLSourceForm.ts
@@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
-import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
+import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@@ -9,12 +9,13 @@ import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
-import { customElement } from "lit/decorators.js";
+import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
BindingTypeEnum,
+ CapabilitiesEnum,
CryptoApi,
DigestAlgorithmEnum,
FlowsApi,
@@ -28,6 +29,9 @@ import {
@customElement("ak-source-saml-form")
export class SAMLSourceForm extends ModelForm {
+ @state()
+ clearIcon = false;
+
loadInstance(pk: string): Promise {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlRetrieve({
slug: pk,
@@ -42,17 +46,37 @@ export class SAMLSourceForm extends ModelForm {
}
}
- send = (data: SAMLSource): Promise => {
+ send = async (data: SAMLSource): Promise => {
+ let source: SAMLSource;
if (this.instance) {
- return new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({
slug: this.instance.slug,
sAMLSourceRequest: data,
});
} else {
- return new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({
+ source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({
sAMLSourceRequest: data,
});
}
+ const c = await config();
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ const icon = this.getFormFiles()["icon"];
+ if (icon || this.clearIcon) {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
+ slug: source.slug,
+ file: icon,
+ clear: this.clearIcon,
+ });
+ }
+ } else {
+ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
+ slug: source.slug,
+ filePathRequest: {
+ url: data.icon || "",
+ },
+ });
+ }
+ return source;
};
renderForm(): TemplateResult {
@@ -126,6 +150,54 @@ export class SAMLSourceForm extends ModelForm {
+ ${until(
+ config().then((c) => {
+ if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
+ return html`
+
+ ${this.instance?.icon
+ ? html`
+
+ ${t`Currently set to:`} ${this.instance?.icon}
+
+ `
+ : html``}
+
+ ${this.instance?.icon
+ ? html`
+
+
+ {
+ const target = ev.target as HTMLInputElement;
+ this.clearIcon = target.checked;
+ }}
+ />
+
+
+
+ ${t`Delete currently set icon.`}
+
+
+ `
+ : html``}`;
+ }
+ return html`
+
+
+ ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
+
+ `;
+ }),
+ )}
${t`Protocol settings`}