sources: add custom icon support (#4022)

* add source icon

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to oauth form

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to other browser sources

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add migration, return icon in UI challenges

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* deduplicate file upload

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-11-16 14:10:10 +01:00 committed by GitHub
parent 1e15d1f538
commit 9f5fb692ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 527 additions and 123 deletions

View File

@ -23,10 +23,15 @@ from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin 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.core.models import Application, User
from authentik.events.models import EventAction from authentik.events.models import EventAction
from authentik.events.utils import sanitize_dict 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.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
@ -224,21 +229,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def set_icon(self, request: Request, slug: str): def set_icon(self, request: Request, slug: str):
"""Set application icon""" """Set application icon"""
app: Application = self.get_object() app: Application = self.get_object()
icon = request.FILES.get("file", None) return set_file(request, app, "meta_icon")
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()
@permission_required("authentik_core.change_application") @permission_required("authentik_core.change_application")
@extend_schema( @extend_schema(
@ -258,12 +249,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def set_icon_url(self, request: Request, slug: str): def set_icon_url(self, request: Request, slug: str):
"""Set application icon (as URL)""" """Set application icon (as URL)"""
app: Application = self.get_object() app: Application = self.get_object()
url = request.data.get("url", None) return set_file_url(request, app, "meta_icon")
if url is None:
return HttpResponseBadRequest()
app.meta_icon.name = url
app.save()
return Response({})
@permission_required("authentik_core.view_application", ["authentik_events.view_event"]) @permission_required("authentik_core.view_application", ["authentik_events.view_event"])
@extend_schema(responses={200: CoordinateSerializer(many=True)}) @extend_schema(responses={200: CoordinateSerializer(many=True)})

View File

@ -2,10 +2,11 @@
from typing import Iterable from typing import Iterable
from django_filters.rest_framework import DjangoFilterBackend 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 import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ReadOnlyField, SerializerMethodField 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 structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions 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.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer 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.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
@ -28,6 +36,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
managed = ReadOnlyField() managed = ReadOnlyField()
component = SerializerMethodField() component = SerializerMethodField()
icon = ReadOnlyField(source="get_icon")
def get_component(self, obj: Source) -> str: def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object""" """Get object component so that we know how to edit the object"""
@ -54,6 +63,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"user_matching_mode", "user_matching_mode",
"managed", "managed",
"user_path_template", "user_path_template",
"icon",
] ]
@ -75,6 +85,49 @@ class SourceViewSet(
def get_queryset(self): # pragma: no cover def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses() 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)}) @extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[]) @action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response: def types(self, request: Request) -> Response:

View File

@ -2,7 +2,7 @@
from typing import Any from typing import Any
from django.db.models import Model 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 from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
@ -23,19 +23,6 @@ class PassiveSerializer(Serializer):
return Model() 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): class MetaNameSerializer(PassiveSerializer):
"""Add verbose names to response""" """Add verbose names to response"""

View File

@ -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/"
),
),
]

View File

@ -421,6 +421,12 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=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( authentication_flow = models.ForeignKey(
"authentik_flows.Flow", "authentik_flows.Flow",
@ -454,6 +460,16 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
objects = InheritanceManager() 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: def get_user_path(self) -> str:
"""Get user path, fallback to default for formatting errors""" """Get user path, fallback to default for formatting errors"""
try: try:

View File

@ -1,7 +1,6 @@
"""Flow API Views""" """Flow API Views"""
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponse from django.http import HttpResponse
from django.http.response import HttpResponseBadRequest
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes 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.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
CacheSerializer,
FilePathSerializer,
FileUploadSerializer,
LinkSerializer,
PassiveSerializer,
)
from authentik.events.utils import sanitize_dict from authentik.events.utils import sanitize_dict
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key 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.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 from authentik.lib.views import bad_request_message
LOGGER = get_logger() LOGGER = get_logger()
@ -249,25 +248,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def set_background(self, request: Request, slug: str): def set_background(self, request: Request, slug: str):
"""Set Flow background""" """Set Flow background"""
flow: Flow = self.get_object() flow: Flow = self.get_object()
background = request.FILES.get("file", None) return set_file(request, flow, "background")
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()
@permission_required("authentik_core.change_application") @permission_required("authentik_core.change_application")
@extend_schema( @extend_schema(
@ -287,12 +268,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def set_background_url(self, request: Request, slug: str): def set_background_url(self, request: Request, slug: str):
"""Set Flow background (as URL)""" """Set Flow background (as URL)"""
flow: Flow = self.get_object() flow: Flow = self.get_object()
url = request.data.get("url", None) return set_file_url(request, flow, "background")
if not url:
return HttpResponseBadRequest()
flow.background.name = url
flow.save()
return Response({})
@extend_schema( @extend_schema(
responses={ responses={

View File

@ -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({})

View File

@ -35,6 +35,7 @@ class OAuthSourceSerializer(SourceSerializer):
provider_type = ChoiceField(choices=registry.get_name_tuple()) provider_type = ChoiceField(choices=registry.get_name_tuple())
callback_url = SerializerMethodField() callback_url = SerializerMethodField()
type = SerializerMethodField()
def get_callback_url(self, instance: OAuthSource) -> str: def get_callback_url(self, instance: OAuthSource) -> str:
"""Get OAuth Callback URL""" """Get OAuth Callback URL"""
@ -46,8 +47,6 @@ class OAuthSourceSerializer(SourceSerializer):
return relative_url return relative_url
return self.context["request"].build_absolute_uri(relative_url) return self.context["request"].build_absolute_uri(relative_url)
type = SerializerMethodField()
@extend_schema_field(SourceTypeSerializer) @extend_schema_field(SourceTypeSerializer)
def get_type(self, instance: OAuthSource) -> SourceTypeSerializer: def get_type(self, instance: OAuthSource) -> SourceTypeSerializer:
"""Get source's type configuration""" """Get source's type configuration"""

View File

@ -75,15 +75,20 @@ class OAuthSource(Source):
def ui_login_button(self, request: HttpRequest) -> UILoginButton: def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.type provider_type = self.type
provider = provider_type() provider = provider_type()
icon = self.get_icon
if not icon:
icon = provider.icon_url()
return UILoginButton( return UILoginButton(
name=self.name, name=self.name,
icon_url=provider.icon_url(),
challenge=provider.login_challenge(self, request), challenge=provider.login_challenge(self, request),
icon_url=icon,
) )
def ui_user_settings(self) -> Optional[UserSettingSerializer]: def ui_user_settings(self) -> Optional[UserSettingSerializer]:
provider_type = self.type provider_type = self.type
provider = provider_type() icon = self.get_icon
if not icon:
icon = provider_type().icon_url()
return UserSettingSerializer( return UserSettingSerializer(
data={ data={
"title": self.name, "title": self.name,
@ -92,7 +97,7 @@ class OAuthSource(Source):
"authentik_sources_oauth:oauth-client-login", "authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug}, kwargs={"source_slug": self.slug},
), ),
"icon_url": provider.icon_url(), "icon_url": icon,
} }
) )

View File

@ -64,6 +64,9 @@ class PlexSource(Source):
return PlexSourceSerializer return PlexSourceSerializer
def ui_login_button(self, request: HttpRequest) -> UILoginButton: def ui_login_button(self, request: HttpRequest) -> UILoginButton:
icon = self.get_icon
if not icon:
icon = static("authentik/sources/plex.svg")
return UILoginButton( return UILoginButton(
challenge=PlexAuthenticationChallenge( challenge=PlexAuthenticationChallenge(
{ {
@ -73,17 +76,20 @@ class PlexSource(Source):
"slug": self.slug, "slug": self.slug,
} }
), ),
icon_url=static("authentik/sources/plex.svg"), icon_url=icon,
name=self.name, name=self.name,
) )
def ui_user_settings(self) -> Optional[UserSettingSerializer]: def ui_user_settings(self) -> Optional[UserSettingSerializer]:
icon = self.get_icon
if not icon:
icon = static("authentik/sources/plex.svg")
return UserSettingSerializer( return UserSettingSerializer(
data={ data={
"title": self.name, "title": self.name,
"component": "ak-user-settings-source-plex", "component": "ak-user-settings-source-plex",
"configure_url": self.client_id, "configure_url": self.client_id,
"icon_url": static("authentik/sources/plex.svg"), "icon_url": icon,
} }
) )

View File

@ -40,7 +40,27 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
queryset = SAMLSource.objects.all() queryset = SAMLSource.objects.all()
serializer_class = SAMLSourceSerializer serializer_class = SAMLSourceSerializer
lookup_field = "slug" 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"] search_fields = ["name", "slug"]
ordering = ["name"] ordering = ["name"]

View File

@ -191,9 +191,13 @@ class SAMLSource(Source):
} }
), ),
name=self.name, name=self.name,
icon_url=self.get_icon,
) )
def ui_user_settings(self) -> Optional[UserSettingSerializer]: def ui_user_settings(self) -> Optional[UserSettingSerializer]:
icon = self.get_icon
if not icon:
icon = static(f"authentik/sources/{self.slug}.svg")
return UserSettingSerializer( return UserSettingSerializer(
data={ data={
"title": self.name, "title": self.name,
@ -202,7 +206,7 @@ class SAMLSource(Source):
"authentik_sources_saml:login", "authentik_sources_saml:login",
kwargs={"source_slug": self.slug}, kwargs={"source_slug": self.slug},
), ),
"icon_url": static(f"authentik/sources/{self.slug}.svg"), "icon_url": icon,
} }
) )

View File

@ -15795,6 +15795,69 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' 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/: /sources/all/{slug}/used_by/:
get: get:
operationId: sources_all_used_by_list operationId: sources_all_used_by_list
@ -17092,20 +17155,6 @@ paths:
description: Number of results to return per page. description: Number of results to return per page.
schema: schema:
type: integer 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 - in: query
name: policy_engine_mode name: policy_engine_mode
schema: schema:
@ -17118,15 +17167,6 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: property_mappings
schema:
type: array
items:
type: string
format: uuid
explode: true
style: form
- name: search - name: search
required: false required: false
in: query in: query
@ -17176,10 +17216,6 @@ paths:
- username_link - username_link
description: How the source determines if an existing user should be authenticated description: How the source determines if an existing user should be authenticated
or a new user enrolled. or a new user enrolled.
- in: query
name: user_path_template
schema:
type: string
tags: tags:
- sources - sources
security: security:
@ -28871,6 +28907,10 @@ components:
readOnly: true readOnly: true
user_path_template: user_path_template:
type: string type: string
icon:
type: string
nullable: true
readOnly: true
server_uri: server_uri:
type: string type: string
format: uri format: uri
@ -28933,6 +28973,7 @@ components:
required: required:
- base_dn - base_dn
- component - component
- icon
- managed - managed
- meta_model_name - meta_model_name
- name - name
@ -29662,6 +29703,10 @@ components:
readOnly: true readOnly: true
user_path_template: user_path_template:
type: string type: string
icon:
type: string
nullable: true
readOnly: true
provider_type: provider_type:
$ref: '#/components/schemas/ProviderTypeEnum' $ref: '#/components/schemas/ProviderTypeEnum'
request_token_url: request_token_url:
@ -29707,6 +29752,7 @@ components:
- callback_url - callback_url
- component - component
- consumer_key - consumer_key
- icon
- managed - managed
- meta_model_name - meta_model_name
- name - name
@ -35075,6 +35121,10 @@ components:
readOnly: true readOnly: true
user_path_template: user_path_template:
type: string type: string
icon:
type: string
nullable: true
readOnly: true
client_id: client_id:
type: string type: string
description: Client identifier used to talk to Plex. description: Client identifier used to talk to Plex.
@ -35092,6 +35142,7 @@ components:
description: Plex token used to check friends description: Plex token used to check friends
required: required:
- component - component
- icon
- managed - managed
- meta_model_name - meta_model_name
- name - name
@ -36495,6 +36546,10 @@ components:
readOnly: true readOnly: true
user_path_template: user_path_template:
type: string type: string
icon:
type: string
nullable: true
readOnly: true
pre_authentication_flow: pre_authentication_flow:
type: string type: string
format: uuid format: uuid
@ -36543,6 +36598,7 @@ components:
doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
required: required:
- component - component
- icon
- managed - managed
- meta_model_name - meta_model_name
- name - name
@ -36936,8 +36992,13 @@ components:
readOnly: true readOnly: true
user_path_template: user_path_template:
type: string type: string
icon:
type: string
nullable: true
readOnly: true
required: required:
- component - component
- icon
- managed - managed
- meta_model_name - meta_model_name
- name - name

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; 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 { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; 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 { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
import { import {
CapabilitiesEnum,
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
OAuthSource, OAuthSource,
@ -57,6 +58,9 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
@property({ attribute: false }) @property({ attribute: false })
providerType: SourceType | null = null; providerType: SourceType | null = null;
@state()
clearIcon = false;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated source.`; return t`Successfully updated source.`;
@ -65,18 +69,38 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
} }
} }
send = (data: OAuthSource): Promise<OAuthSource> => { send = async (data: OAuthSource): Promise<OAuthSource> => {
data.providerType = (this.providerType?.slug || "") as ProviderTypeEnum; data.providerType = (this.providerType?.slug || "") as ProviderTypeEnum;
if (this.instance?.slug) { let source: OAuthSource;
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({ if (this.instance) {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
slug: this.instance.slug, slug: this.instance.slug,
patchedOAuthSourceRequest: data, patchedOAuthSourceRequest: data,
}); });
} else { } else {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({ source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
oAuthSourceRequest: data as unknown as OAuthSourceRequest, 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 { renderUrlOptions(): TemplateResult {
@ -282,6 +306,54 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`} ${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; 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 { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { first, randomString } from "@goauthentik/common/utils"; import { first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -9,11 +9,12 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; 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 { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
import { import {
CapabilitiesEnum,
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
PlexSource, PlexSource,
@ -35,6 +36,9 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
}); });
} }
@state()
clearIcon = false;
@property() @property()
plexToken?: string; plexToken?: string;
@ -55,18 +59,38 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
} }
} }
send = (data: PlexSource): Promise<PlexSource> => { send = async (data: PlexSource): Promise<PlexSource> => {
data.plexToken = this.plexToken || ""; data.plexToken = this.plexToken || "";
if (this.instance?.slug) { let source: PlexSource;
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({ if (this.instance) {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
slug: this.instance.slug, slug: this.instance.slug,
plexSourceRequest: data, plexSourceRequest: data,
}); });
} else { } else {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({ source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
plexSourceRequest: data, 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<void> { async doAuth(): Promise<void> {
@ -229,6 +253,54 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`} ${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>

View File

@ -1,5 +1,5 @@
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; 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 { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -9,12 +9,13 @@ import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; 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 { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
import { import {
BindingTypeEnum, BindingTypeEnum,
CapabilitiesEnum,
CryptoApi, CryptoApi,
DigestAlgorithmEnum, DigestAlgorithmEnum,
FlowsApi, FlowsApi,
@ -28,6 +29,9 @@ import {
@customElement("ak-source-saml-form") @customElement("ak-source-saml-form")
export class SAMLSourceForm extends ModelForm<SAMLSource, string> { export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
@state()
clearIcon = false;
loadInstance(pk: string): Promise<SAMLSource> { loadInstance(pk: string): Promise<SAMLSource> {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlRetrieve({ return new SourcesApi(DEFAULT_CONFIG).sourcesSamlRetrieve({
slug: pk, slug: pk,
@ -42,17 +46,37 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
} }
} }
send = (data: SAMLSource): Promise<SAMLSource> => { send = async (data: SAMLSource): Promise<SAMLSource> => {
let source: SAMLSource;
if (this.instance) { if (this.instance) {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({ source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlUpdate({
slug: this.instance.slug, slug: this.instance.slug,
sAMLSourceRequest: data, sAMLSourceRequest: data,
}); });
} else { } else {
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({ source = await new SourcesApi(DEFAULT_CONFIG).sourcesSamlCreate({
sAMLSourceRequest: data, 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 { renderForm(): TemplateResult {
@ -126,6 +150,54 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<label class="pf-c-check__label">
${t`Clear icon`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>