sources/saml: revamp SAML Source (#3785)
* update saml source to use user connections, add all attributes to flow context Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * check for SAML Status in response, add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * package apple icon Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add webui for connections Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
5e620c74f9
commit
363872715d
2
Makefile
2
Makefile
|
@ -53,7 +53,7 @@ migrate:
|
|||
python -m lifecycle.migrate
|
||||
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
go run -v ./cmd/server/
|
||||
|
||||
i18n-extract: i18n-extract-core web-extract
|
||||
|
||||
|
|
|
@ -59,7 +59,8 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
|||
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
||||
from authentik.sources.plex.api.source import PlexSourceViewSet
|
||||
from authentik.sources.plex.api.source_connection import PlexSourceConnectionViewSet
|
||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||
from authentik.sources.saml.api.source import SAMLSourceViewSet
|
||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionViewSet
|
||||
from authentik.stages.authenticator_duo.api import (
|
||||
AuthenticatorDuoStageViewSet,
|
||||
DuoAdminDeviceViewSet,
|
||||
|
@ -138,6 +139,7 @@ router.register("sources/all", SourceViewSet)
|
|||
router.register("sources/user_connections/all", UserSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/saml", UserSAMLSourceConnectionViewSet)
|
||||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/saml", SAMLSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
|
|
|
@ -130,8 +130,8 @@ class TestAuthNRequest(TestCase):
|
|||
http_request.POST = QueryDict(mutable=True)
|
||||
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
|
||||
|
||||
response_parser = ResponseProcessor(self.source)
|
||||
response_parser.parse(http_request)
|
||||
response_parser = ResponseProcessor(self.source, http_request)
|
||||
response_parser.parse()
|
||||
|
||||
def test_request_id_invalid(self):
|
||||
"""Test generated AuthNRequest with invalid request ID"""
|
||||
|
@ -157,10 +157,10 @@ class TestAuthNRequest(TestCase):
|
|||
http_request.POST = QueryDict(mutable=True)
|
||||
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
|
||||
|
||||
response_parser = ResponseProcessor(self.source)
|
||||
response_parser = ResponseProcessor(self.source, http_request)
|
||||
|
||||
with self.assertRaises(MismatchedRequestID):
|
||||
response_parser.parse(http_request)
|
||||
response_parser.parse()
|
||||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
|
|
|
@ -114,9 +114,6 @@ class AppleType(SourceType):
|
|||
access_token_url = "https://appleid.apple.com/auth/token" # nosec
|
||||
profile_url = ""
|
||||
|
||||
def icon_url(self) -> str:
|
||||
return "https://appleid.cdn-apple.com/appleid/button/logo"
|
||||
|
||||
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
|
||||
"""Pre-general all the things required for the JS SDK"""
|
||||
apple_client = AppleOAuthClient(
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"""SAML Source Serializer"""
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
||||
from authentik.core.api.sources import UserSourceConnectionSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.saml.models import UserSAMLSourceConnection
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = UserSAMLSourceConnection
|
||||
fields = ["pk", "user", "source", "identifier"]
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserSAMLSourceConnection.objects.all()
|
||||
serializer_class = UserSAMLSourceConnectionSerializer
|
||||
filterset_fields = ["source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
permission_classes = [OwnerSuperuserPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
ordering = ["source__slug"]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 4.1.2 on 2022-10-14 12:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0022_alter_group_parent"),
|
||||
("authentik_sources_saml", "0011_auto_20210324_0736"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserSAMLSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"usersourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.usersourceconnection",
|
||||
),
|
||||
),
|
||||
("identifier", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User SAML Source Connection",
|
||||
"verbose_name_plural": "User SAML Source Connections",
|
||||
},
|
||||
bases=("authentik_core.usersourceconnection",),
|
||||
),
|
||||
]
|
|
@ -1,13 +1,15 @@
|
|||
"""saml sp models"""
|
||||
from typing import Optional
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import Source
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
|
||||
from authentik.flows.models import Flow
|
||||
|
@ -161,7 +163,7 @@ class SAMLSource(Source):
|
|||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.saml.api import SAMLSourceSerializer
|
||||
from authentik.sources.saml.api.source import SAMLSourceSerializer
|
||||
|
||||
return SAMLSourceSerializer
|
||||
|
||||
|
@ -191,6 +193,19 @@ class SAMLSource(Source):
|
|||
name=self.name,
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
"component": "ak-user-settings-source-saml",
|
||||
"configure_url": reverse(
|
||||
"authentik_sources_saml:login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
"icon_url": static(f"authentik/sources/{self.slug}.svg"),
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"SAML Source {self.name}"
|
||||
|
||||
|
@ -198,3 +213,20 @@ class SAMLSource(Source):
|
|||
|
||||
verbose_name = _("SAML Source")
|
||||
verbose_name_plural = _("SAML Sources")
|
||||
|
||||
|
||||
class UserSAMLSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured SAML Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer
|
||||
|
||||
return UserSAMLSourceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("User SAML Source Connection")
|
||||
verbose_name_plural = _("User SAML Source Connections")
|
||||
|
|
|
@ -7,7 +7,7 @@ import xmlsec
|
|||
from defusedxml.lxml import fromstring
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
|
@ -18,17 +18,9 @@ from authentik.core.models import (
|
|||
USER_ATTRIBUTE_SOURCES,
|
||||
User,
|
||||
)
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.sources.saml.exceptions import (
|
||||
InvalidSignature,
|
||||
|
@ -36,9 +28,11 @@ from authentik.sources.saml.exceptions import (
|
|||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.models import SAMLSource, UserSAMLSourceConnection
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
NS_SAML_PROTOCOL,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
|
@ -46,9 +40,6 @@ from authentik.sources.saml.processors.constants import (
|
|||
SAML_NAME_ID_FORMAT_X509,
|
||||
)
|
||||
from authentik.sources.saml.processors.request import SESSION_KEY_REQUEST_ID
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_login.stage import BACKEND_INBUILT
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
|
@ -67,14 +58,14 @@ class ResponseProcessor:
|
|||
|
||||
_http_request: HttpRequest
|
||||
|
||||
def __init__(self, source: SAMLSource):
|
||||
def __init__(self, source: SAMLSource, request: HttpRequest):
|
||||
self._source = source
|
||||
|
||||
def parse(self, request: HttpRequest):
|
||||
"""Check if `request` contains SAML Response data, parse and validate it."""
|
||||
self._http_request = request
|
||||
|
||||
def parse(self):
|
||||
"""Check if `request` contains SAML Response data, parse and validate it."""
|
||||
# First off, check if we have any SAML Data at all.
|
||||
raw_response = request.POST.get("SAMLResponse", None)
|
||||
raw_response = self._http_request.POST.get("SAMLResponse", None)
|
||||
if not raw_response:
|
||||
raise MissingSAMLResponse("Request does not contain 'SAMLResponse'")
|
||||
# Check if response is compressed, b64 decode it
|
||||
|
@ -83,7 +74,8 @@ class ResponseProcessor:
|
|||
|
||||
if self._source.signing_kp:
|
||||
self._verify_signed()
|
||||
self._verify_request_id(request)
|
||||
self._verify_request_id()
|
||||
self._verify_status()
|
||||
|
||||
def _verify_signed(self):
|
||||
"""Verify SAML Response's Signature"""
|
||||
|
@ -109,7 +101,7 @@ class ResponseProcessor:
|
|||
raise InvalidSignature from exc
|
||||
LOGGER.debug("Successfully verified signautre")
|
||||
|
||||
def _verify_request_id(self, request: HttpRequest):
|
||||
def _verify_request_id(self):
|
||||
if self._source.allow_idp_initiated:
|
||||
# If IdP-initiated SSO flows are enabled, we want to cache the Response ID
|
||||
# somewhat mitigate replay attacks
|
||||
|
@ -119,14 +111,26 @@ class ResponseProcessor:
|
|||
seen_ids.append(self._root.attrib["ID"])
|
||||
cache.set(CACHE_SEEN_REQUEST_ID % self._source.pk, seen_ids)
|
||||
return
|
||||
if SESSION_KEY_REQUEST_ID not in request.session or "InResponseTo" not in self._root.attrib:
|
||||
if (
|
||||
SESSION_KEY_REQUEST_ID not in self._http_request.session
|
||||
or "InResponseTo" not in self._root.attrib
|
||||
):
|
||||
raise MismatchedRequestID(
|
||||
"Missing InResponseTo and IdP-initiated Logins are not allowed"
|
||||
)
|
||||
if request.session[SESSION_KEY_REQUEST_ID] != self._root.attrib["InResponseTo"]:
|
||||
if self._http_request.session[SESSION_KEY_REQUEST_ID] != self._root.attrib["InResponseTo"]:
|
||||
raise MismatchedRequestID("Mismatched request ID")
|
||||
|
||||
def _handle_name_id_transient(self, request: HttpRequest) -> HttpResponse:
|
||||
def _verify_status(self):
|
||||
"""Check for SAML Status elements"""
|
||||
status = self._root.find(f"{{{NS_SAML_PROTOCOL}}}Status")
|
||||
if status is None:
|
||||
return
|
||||
message = status.find(f"{{{NS_SAML_PROTOCOL}}}StatusMessage")
|
||||
if message is not None:
|
||||
raise ValueError(message.text)
|
||||
|
||||
def _handle_name_id_transient(self) -> SourceFlowManager:
|
||||
"""Handle a NameID with the Format of Transient. This is a bit more complex than other
|
||||
formats, as we need to create a temporary User that is used in the session. This
|
||||
user has an attribute that refers to our Source for cleanup. The user is also deleted
|
||||
|
@ -151,24 +155,23 @@ class ResponseProcessor:
|
|||
LOGGER.debug("Created temporary user for NameID Transient", username=name_id)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
return self._flow_response(
|
||||
request,
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: user,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
},
|
||||
UserSAMLSourceConnection.objects.create(source=self._source, user=user, identifier=name_id)
|
||||
return SAMLSourceFlowManager(
|
||||
self._source,
|
||||
self._http_request,
|
||||
name_id,
|
||||
delete_none_keys(self.get_attributes()),
|
||||
)
|
||||
|
||||
def _get_name_id(self) -> "Element":
|
||||
"""Get NameID Element"""
|
||||
assertion = self._root.find("{urn:oasis:names:tc:SAML:2.0:assertion}Assertion")
|
||||
if not assertion:
|
||||
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
if assertion is None:
|
||||
raise ValueError("Assertion element not found")
|
||||
subject = assertion.find("{urn:oasis:names:tc:SAML:2.0:assertion}Subject")
|
||||
if not subject:
|
||||
subject = assertion.find(f"{{{NS_SAML_ASSERTION}}}Subject")
|
||||
if subject is None:
|
||||
raise ValueError("Subject element not found")
|
||||
name_id = subject.find("{urn:oasis:names:tc:SAML:2.0:assertion}NameID")
|
||||
name_id = subject.find(f"{{{NS_SAML_ASSERTION}}}NameID")
|
||||
if name_id is None:
|
||||
raise ValueError("NameID element not found")
|
||||
return name_id
|
||||
|
@ -195,7 +198,27 @@ class ResponseProcessor:
|
|||
f"Assertion contains NameID with unsupported format {_format}."
|
||||
)
|
||||
|
||||
def prepare_flow(self, request: HttpRequest) -> HttpResponse:
|
||||
def get_attributes(self) -> dict[str, list[str] | str]:
|
||||
"""Get all attributes sent"""
|
||||
attributes = {}
|
||||
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
if not assertion:
|
||||
raise ValueError("Assertion element not found")
|
||||
attribute_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
|
||||
if not attribute_statement:
|
||||
raise ValueError("Attribute statement element not found")
|
||||
# Get all attributes and their values into a dict
|
||||
for attribute in attribute_statement.iterchildren():
|
||||
key = attribute.attrib["Name"]
|
||||
attributes.setdefault(key, [])
|
||||
for value in attribute.iterchildren():
|
||||
attributes[key].append(value.text)
|
||||
# Flatten all lists in the dict
|
||||
for key, value in attributes.items():
|
||||
attributes[key] = BaseEvaluator.expr_flatten(value)
|
||||
return attributes
|
||||
|
||||
def prepare_flow_manager(self) -> SourceFlowManager:
|
||||
"""Prepare flow plan depending on whether or not the user exists"""
|
||||
name_id = self._get_name_id()
|
||||
# Sanity check, show a warning if NameIDPolicy doesn't match what we go
|
||||
|
@ -207,38 +230,17 @@ class ResponseProcessor:
|
|||
)
|
||||
# transient NameIDs are handled separately as they don't have to go through flows.
|
||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||
return self._handle_name_id_transient(request)
|
||||
return self._handle_name_id_transient()
|
||||
|
||||
name_id_filter = self._get_name_id_filter()
|
||||
matching_users = User.objects.filter(**name_id_filter)
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
if matching_users.exists():
|
||||
# User exists already, switch to authentication flow
|
||||
return self._flow_response(
|
||||
request,
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
},
|
||||
)
|
||||
return self._flow_response(
|
||||
request,
|
||||
self._source.enrollment_flow,
|
||||
**{PLAN_CONTEXT_PROMPT: delete_none_keys(name_id_filter)},
|
||||
return SAMLSourceFlowManager(
|
||||
self._source,
|
||||
self._http_request,
|
||||
name_id.text,
|
||||
delete_none_keys(self.get_attributes()),
|
||||
)
|
||||
|
||||
def _flow_response(self, request: HttpRequest, flow: Flow, **kwargs) -> HttpResponse:
|
||||
kwargs[PLAN_CONTEXT_SSO] = True
|
||||
kwargs[PLAN_CONTEXT_SOURCE] = self._source
|
||||
request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs)
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
request.GET,
|
||||
flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
class SAMLSourceFlowManager(SourceFlowManager):
|
||||
"""Source flow manager for SAML Sources"""
|
||||
|
||||
connection_type = UserSAMLSourceConnection
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.test import RequestFactory, TestCase
|
|||
from lxml import etree # nosec
|
||||
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
|
@ -14,17 +15,17 @@ class TestMetadataProcessor(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_metadata_schema(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
signing_kp=create_test_cert(),
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
def test_metadata_schema(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
metadata = lxml_from_string(xml)
|
||||
|
||||
schema = etree.XMLSchema(etree.parse("xml/saml-schema-metadata-2.0.xsd")) # nosec
|
||||
|
@ -32,38 +33,23 @@ class TestMetadataProcessor(TestCase):
|
|||
|
||||
def test_metadata_consistent(self):
|
||||
"""Test Metadata generation being consistent (xml stays the same)"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=create_test_cert(),
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
xml_a = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
xml_b = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
xml_a = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
xml_b = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
self.assertEqual(xml_a, xml_b)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=create_test_cert(),
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
metadata = ElementTree.fromstring(xml)
|
||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
||||
|
||||
def test_metadata_without_signautre(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
self.source.signing_kp = None
|
||||
self.source.save()
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
metadata = ElementTree.fromstring(xml)
|
||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
"""SAML Source tests"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.response import ResponseProcessor
|
||||
|
||||
RESPONSE_ERROR = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_ee7a8865ac457e7b22cb4f16b39ceca9" IssueInstant="2022-10-14T13:52:04.479Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester">
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied"></saml2p:StatusCode>
|
||||
</saml2p:StatusCode>
|
||||
<saml2p:StatusMessage>Invalid request, ACS Url in request http://localhost:9000/source/saml/google/acs/ doesn't match configured ACS Url https://127.0.0.1:9443/source/saml/google/acs/.</saml2p:StatusMessage>
|
||||
</saml2p:Status>
|
||||
</saml2p:Response>
|
||||
"""
|
||||
|
||||
RESPONSE_SUCCESS = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_1e17063957f10819a5a8e147971fec22" InResponseTo="_157fb504b59f4ae3919f74896a6b8565" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></saml2p:StatusCode>
|
||||
</saml2p:Status>
|
||||
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_346001c5708ffd118c40edbc0c72fc60" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2:Subject>
|
||||
<saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">jens@beryju.org</saml2:NameID>
|
||||
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml2:SubjectConfirmationData InResponseTo="_157fb504b59f4ae3919f74896a6b8565" NotOnOrAfter="2022-10-14T14:16:49.590Z" Recipient="https://127.0.0.1:9443/source/saml/google/acs/"></saml2:SubjectConfirmationData>
|
||||
</saml2:SubjectConfirmation>
|
||||
</saml2:Subject>
|
||||
<saml2:Conditions NotBefore="2022-10-14T14:06:49.590Z" NotOnOrAfter="2022-10-14T14:16:49.590Z">
|
||||
<saml2:AudienceRestriction>
|
||||
<saml2:Audience>https://accounts.google.com/o/saml2?idpid=</saml2:Audience>
|
||||
</saml2:AudienceRestriction>
|
||||
</saml2:Conditions>
|
||||
<saml2:AttributeStatement>
|
||||
<saml2:Attribute Name="name">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="sn">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">bar</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="email">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo@bar.baz</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
</saml2:AttributeStatement>
|
||||
<saml2:AuthnStatement AuthnInstant="2022-10-14T12:16:21.000Z" SessionIndex="_346001c5708ffd118c40edbc0c72fc60">
|
||||
<saml2:AuthnContext>
|
||||
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
|
||||
</saml2:AuthnContext>
|
||||
</saml2:AuthnStatement>
|
||||
</saml2:Assertion>
|
||||
</saml2p:Response>
|
||||
"""
|
||||
|
||||
|
||||
class TestResponseProcessor(TestCase):
|
||||
"""Test ResponseProcessor"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
allow_idp_initiated=True,
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
def test_status_error(self):
|
||||
"""Test error status"""
|
||||
request = self.factory.post(
|
||||
"/", data={"SAMLResponse": b64encode(RESPONSE_ERROR.encode()).decode()}
|
||||
)
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
(
|
||||
"Invalid request, ACS Url in request http://localhost:9000/source/saml/google/acs/ "
|
||||
"doesn't match configured ACS Url https://127.0.0.1:9443/source/saml/google/acs/."
|
||||
),
|
||||
):
|
||||
ResponseProcessor(self.source, request).parse()
|
||||
|
||||
def test_success(self):
|
||||
"""Test success"""
|
||||
request = self.factory.post(
|
||||
"/", data={"SAMLResponse": b64encode(RESPONSE_SUCCESS.encode()).decode()}
|
||||
)
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
sfm = parser.prepare_flow_manager()
|
||||
self.assertEqual(sfm.enroll_info, {"email": "foo@bar.baz", "name": "foo", "sn": "bar"})
|
|
@ -153,16 +153,16 @@ class ACSView(View):
|
|||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
raise Http404
|
||||
processor = ResponseProcessor(source)
|
||||
processor = ResponseProcessor(source, request)
|
||||
try:
|
||||
processor.parse(request)
|
||||
processor.parse()
|
||||
except MissingSAMLResponse as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
except VerificationError as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
try:
|
||||
return processor.prepare_flow(request)
|
||||
return processor.prepare_flow_manager().get_flow()
|
||||
except (UnsupportedNameIDFormat, ValueError) as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
"authentik_sources_plex.plexsource",
|
||||
"authentik_sources_plex.plexsourceconnection",
|
||||
"authentik_sources_saml.samlsource",
|
||||
"authentik_sources_saml.usersamlsourceconnection",
|
||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.duodevice",
|
||||
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
||||
|
|
341
schema.yml
341
schema.yml
|
@ -18164,6 +18164,270 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/user_connections/saml/:
|
||||
get:
|
||||
operationId: sources_user_connections_saml_list
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
required: false
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page_size
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: source__slug
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedUserSAMLSourceConnectionList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_user_connections_saml_create
|
||||
description: Source Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/user_connections/saml/{id}/:
|
||||
get:
|
||||
operationId: sources_user_connections_saml_retrieve
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User SAML Source Connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_user_connections_saml_update
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User SAML Source Connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_user_connections_saml_partial_update
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User SAML Source Connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedUserSAMLSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_user_connections_saml_destroy
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User SAML Source Connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/user_connections/saml/{id}/used_by/:
|
||||
get:
|
||||
operationId: sources_user_connections_saml_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User SAML Source Connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UsedBy'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/all/:
|
||||
get:
|
||||
operationId: stages_all_list
|
||||
|
@ -32190,6 +32454,41 @@ components:
|
|||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedUserSAMLSourceConnectionList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
type: object
|
||||
properties:
|
||||
next:
|
||||
type: number
|
||||
previous:
|
||||
type: number
|
||||
count:
|
||||
type: number
|
||||
current:
|
||||
type: number
|
||||
total_pages:
|
||||
type: number
|
||||
start_index:
|
||||
type: number
|
||||
end_index:
|
||||
type: number
|
||||
required:
|
||||
- next
|
||||
- previous
|
||||
- count
|
||||
- current
|
||||
- total_pages
|
||||
- start_index
|
||||
- end_index
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UserSAMLSourceConnection'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedUserSourceConnectionList:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -34432,6 +34731,15 @@ components:
|
|||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedUserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedUserWriteStageRequest:
|
||||
type: object
|
||||
description: UserWriteStage Serializer
|
||||
|
@ -37376,6 +37684,39 @@ components:
|
|||
- groups
|
||||
- name
|
||||
- username
|
||||
UserSAMLSourceConnection:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
readOnly: true
|
||||
title: ID
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
required:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- user
|
||||
UserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- identifier
|
||||
- user
|
||||
UserSelf:
|
||||
type: object
|
||||
description: User Serializer for information a user can retrieve about themselves
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
|
||||
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 663 B |
|
@ -1,3 +1,4 @@
|
|||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
SAMLSource,
|
||||
SignatureAlgorithmEnum,
|
||||
SourcesApi,
|
||||
UserMatchingModeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-source-saml-form")
|
||||
|
@ -81,6 +83,49 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
<label class="pf-c-check__label"> ${t`Enabled`} </label>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`User matching mode`}
|
||||
?required=${true}
|
||||
name="userMatchingMode"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${UserMatchingModeEnum.Identifier}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.Identifier}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.Identifier)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.EmailLink}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.EmailLink}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailLink)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.EmailDeny}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.EmailDeny}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailDeny)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.UsernameLink}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.UsernameLink}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameLink)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.UsernameDeny}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.UsernameDeny}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameDeny)}
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
|
|
@ -534,7 +534,7 @@ export class FlowExecutor extends AKElement implements StageHost {
|
|||
? html`
|
||||
<li>
|
||||
<a
|
||||
href="https://unsplash.com/@impatrickt"
|
||||
href="https://unsplash.com/@brendan_k_steeves"
|
||||
>${t`Background image`}</a
|
||||
>
|
||||
</li>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AKElement } from "@goauthentik/elements/Base";
|
|||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/user/user-settings/sources/SourceSettingsOAuth";
|
||||
import "@goauthentik/user/user-settings/sources/SourceSettingsPlex";
|
||||
import "@goauthentik/user/user-settings/sources/SourceSettingsSAML";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
|
@ -95,6 +96,15 @@ export class UserSourceSettingsPage extends AKElement {
|
|||
.configureUrl=${source.configureUrl}
|
||||
>
|
||||
</ak-user-settings-source-plex>`;
|
||||
case "ak-user-settings-source-saml":
|
||||
return html`<ak-user-settings-source-saml
|
||||
class="pf-c-data-list__item-row"
|
||||
objectId=${source.objectUid}
|
||||
title=${source.title}
|
||||
connectionPk=${connectionPk}
|
||||
.configureUrl=${source.configureUrl}
|
||||
>
|
||||
</ak-user-settings-source-saml>`;
|
||||
default:
|
||||
return html`<p>${t`Error: unsupported source settings: ${source.component}`}</p>`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import "@goauthentik/elements/Spinner";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import { BaseUserSettings } from "@goauthentik/user/user-settings/BaseUserSettings";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { SourcesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-user-settings-source-saml")
|
||||
export class SourceSettingsSAML extends BaseUserSettings {
|
||||
@property()
|
||||
title!: string;
|
||||
|
||||
@property({ type: Number })
|
||||
connectionPk = 0;
|
||||
|
||||
render(): TemplateResult {
|
||||
if (this.connectionPk === -1) {
|
||||
return html`<ak-spinner></ak-spinner>`;
|
||||
}
|
||||
if (this.connectionPk > 0) {
|
||||
return html`<button
|
||||
class="pf-c-button pf-m-danger"
|
||||
@click=${() => {
|
||||
return new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesUserConnectionsSamlDestroy({
|
||||
id: this.connectionPk,
|
||||
})
|
||||
.then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.info,
|
||||
message: t`Successfully disconnected source`,
|
||||
});
|
||||
})
|
||||
.catch((exc) => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`Failed to disconnected source: ${exc}`,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.parentElement?.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`Disconnect`}
|
||||
</button>`;
|
||||
}
|
||||
return html`<a
|
||||
class="pf-c-button pf-m-primary"
|
||||
href="${ifDefined(this.configureUrl)}${AndNext(
|
||||
`/if/user/#/settings;${JSON.stringify({ page: "page-sources" })}`,
|
||||
)}"
|
||||
>
|
||||
${t`Connect`}
|
||||
</a>`;
|
||||
}
|
||||
}
|
Reference in New Issue