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:
parent
1e15d1f538
commit
9f5fb692ba
|
@ -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)})
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
20
authentik/core/migrations/0024_source_icon.py
Normal file
20
authentik/core/migrations/0024_source_icon.py
Normal 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/"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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:
|
||||
|
|
|
@ -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={
|
||||
|
|
55
authentik/lib/utils/file.py
Normal file
55
authentik/lib/utils/file.py
Normal 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({})
|
|
@ -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"""
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
115
schema.yml
115
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
|
||||
|
|
|
@ -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<OAuthSource, string> {
|
|||
@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<OAuthSource, string> {
|
|||
}
|
||||
}
|
||||
|
||||
send = (data: OAuthSource): Promise<OAuthSource> => {
|
||||
send = async (data: OAuthSource): Promise<OAuthSource> => {
|
||||
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<OAuthSource, string> {
|
|||
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||
</p>
|
||||
</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}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
|
|
@ -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<PlexSource, string> {
|
|||
});
|
||||
}
|
||||
|
||||
@state()
|
||||
clearIcon = false;
|
||||
|
||||
@property()
|
||||
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 || "";
|
||||
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<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.`}
|
||||
</p>
|
||||
</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}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
|
|
@ -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<SAMLSource, string> {
|
||||
@state()
|
||||
clearIcon = false;
|
||||
|
||||
loadInstance(pk: string): Promise<SAMLSource> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesSamlRetrieve({
|
||||
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) {
|
||||
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<SAMLSource, string> {
|
|||
</option>
|
||||
</select>
|
||||
</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}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
|
Reference in a new issue