core: applications backchannel provider (#5449)
* backchannel applications Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add webui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include assigned app in provider Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve backchannel provider list display Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make ldap provider compatible Signed-off-by: Jens Langhammer <jens@goauthentik.io> * show backchannel providers in app view Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make backchannel required for SCIM Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup api Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Apply suggestions from code review Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
parent
9f4be4d150
commit
7acd0558f5
|
@ -52,6 +52,9 @@ class ApplicationSerializer(ModelSerializer):
|
|||
|
||||
launch_url = SerializerMethodField()
|
||||
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
|
||||
backchannel_providers_obj = ProviderSerializer(
|
||||
source="backchannel_providers", required=False, read_only=True, many=True
|
||||
)
|
||||
|
||||
meta_icon = ReadOnlyField(source="get_meta_icon")
|
||||
|
||||
|
@ -75,6 +78,8 @@ class ApplicationSerializer(ModelSerializer):
|
|||
"slug",
|
||||
"provider",
|
||||
"provider_obj",
|
||||
"backchannel_providers",
|
||||
"backchannel_providers_obj",
|
||||
"launch_url",
|
||||
"open_in_new_tab",
|
||||
"meta_launch_url",
|
||||
|
@ -86,6 +91,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||
]
|
||||
extra_kwargs = {
|
||||
"meta_icon": {"read_only": True},
|
||||
"backchannel_providers": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Provider API Views"""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
|
@ -20,6 +22,8 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
|
||||
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
|
||||
|
||||
component = SerializerMethodField()
|
||||
|
||||
|
@ -40,6 +44,8 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||
"component",
|
||||
"assigned_application_slug",
|
||||
"assigned_application_name",
|
||||
"assigned_backchannel_application_slug",
|
||||
"assigned_backchannel_application_name",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
|
@ -49,6 +55,22 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||
}
|
||||
|
||||
|
||||
class ProviderFilter(FilterSet):
|
||||
"""Filter for groups"""
|
||||
|
||||
application__isnull = BooleanFilter(
|
||||
field_name="application",
|
||||
lookup_expr="isnull",
|
||||
)
|
||||
backchannel_only = BooleanFilter(
|
||||
method="filter_backchannel_only",
|
||||
)
|
||||
|
||||
def filter_backchannel_only(self, queryset, name, value):
|
||||
"""Only return backchannel providers"""
|
||||
return queryset.filter(is_backchannel=value)
|
||||
|
||||
|
||||
class ProviderViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
|
@ -60,9 +82,7 @@ class ProviderViewSet(
|
|||
|
||||
queryset = Provider.objects.none()
|
||||
serializer_class = ProviderSerializer
|
||||
filterset_fields = {
|
||||
"application": ["isnull"],
|
||||
}
|
||||
filterset_class = ProviderFilter
|
||||
search_fields = [
|
||||
"name",
|
||||
"application__name",
|
||||
|
@ -78,6 +98,8 @@ class ProviderViewSet(
|
|||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: Provider
|
||||
if subclass._meta.abstract:
|
||||
continue
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# Generated by Django 4.1.7 on 2023-04-30 17:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.apps.registry import Apps
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError, migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.core.models import BackchannelProvider
|
||||
|
||||
for model in BackchannelProvider.__subclasses__():
|
||||
try:
|
||||
for obj in model.objects.all():
|
||||
obj.is_backchannel = True
|
||||
obj.save()
|
||||
except (DatabaseError, InternalError, ProgrammingError):
|
||||
# The model might not have been migrated yet/doesn't exist yet
|
||||
# so we don't need to worry about backporting the data
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_core", "0028_provider_authentication_flow"),
|
||||
("authentik_providers_ldap", "0002_ldapprovider_bind_mode"),
|
||||
("authentik_providers_scim", "0006_rename_parent_group_scimprovider_filter_group"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="backchannel_application",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Accessed from applications; optional backchannel providers for protocols like LDAP and SCIM.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="backchannel_providers",
|
||||
to="authentik_core.application",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="is_backchannel",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.RunPython(backport_is_backchannel),
|
||||
]
|
|
@ -270,6 +270,20 @@ class Provider(SerializerModel):
|
|||
|
||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
||||
|
||||
backchannel_application = models.ForeignKey(
|
||||
"Application",
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_(
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
"like LDAP and SCIM."
|
||||
),
|
||||
related_name="backchannel_providers",
|
||||
)
|
||||
|
||||
is_backchannel = models.BooleanField(default=False)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
|
@ -292,6 +306,26 @@ class Provider(SerializerModel):
|
|||
return str(self.name)
|
||||
|
||||
|
||||
class BackchannelProvider(Provider):
|
||||
"""Base class for providers that augment other providers, for example LDAP and SCIM.
|
||||
Multiple of these providers can be configured per application, they may not use the application
|
||||
slug in URLs as an application may have multiple instances of the same
|
||||
type of Backchannel provider
|
||||
|
||||
They can use the application's policies and metadata"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
raise NotImplementedError
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Application(SerializerModel, PolicyBindingModel):
|
||||
"""Every Application which uses authentik for authentication/identification/authorization
|
||||
needs an Application record. Other authentication types can subclass this Model to
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.contrib.sessions.backends.cache import KEY_PREFIX
|
|||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession
|
||||
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
|
@ -54,3 +54,11 @@ def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSe
|
|||
"""Delete session when authenticated session is deleted"""
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
|
||||
"""Ensure backchannel providers have is_backchannel set to true"""
|
||||
if not isinstance(instance, BackchannelProvider):
|
||||
return
|
||||
instance.is_backchannel = True
|
||||
|
|
|
@ -139,6 +139,8 @@ class TestApplicationsAPI(APITestCase):
|
|||
"verbose_name": "OAuth2/OpenID Provider",
|
||||
"verbose_name_plural": "OAuth2/OpenID Providers",
|
||||
},
|
||||
"backchannel_providers": [],
|
||||
"backchannel_providers_obj": [],
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"open_in_new_tab": True,
|
||||
|
@ -189,6 +191,8 @@ class TestApplicationsAPI(APITestCase):
|
|||
"verbose_name": "OAuth2/OpenID Provider",
|
||||
"verbose_name_plural": "OAuth2/OpenID Providers",
|
||||
},
|
||||
"backchannel_providers": [],
|
||||
"backchannel_providers_obj": [],
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"open_in_new_tab": True,
|
||||
|
@ -210,6 +214,8 @@ class TestApplicationsAPI(APITestCase):
|
|||
"policy_engine_mode": "any",
|
||||
"provider": None,
|
||||
"provider_obj": None,
|
||||
"backchannel_providers": [],
|
||||
"backchannel_providers_obj": [],
|
||||
"slug": "denied",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -53,9 +53,8 @@ def provider_tester_factory(test_model: type[Stage]) -> Callable:
|
|||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertIsNotNone(model_class.component)
|
||||
|
||||
return tester
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""LDAPProvider API Views"""
|
||||
from rest_framework.fields import CharField, ListField
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
|
@ -54,9 +54,15 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
|||
class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
"""LDAPProvider Serializer"""
|
||||
|
||||
application_slug = CharField(source="application.slug")
|
||||
application_slug = SerializerMethodField()
|
||||
bind_flow_slug = CharField(source="authorization_flow.slug")
|
||||
|
||||
def get_application_slug(self, instance: LDAPProvider) -> str:
|
||||
"""Prioritise backchannel slug over direct application slug"""
|
||||
if instance.backchannel_application:
|
||||
return instance.backchannel_application.slug
|
||||
return instance.application.slug
|
||||
|
||||
class Meta:
|
||||
model = LDAPProvider
|
||||
fields = [
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.db import models
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import Group, Provider
|
||||
from authentik.core.models import BackchannelProvider, Group
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
|
@ -17,7 +17,7 @@ class APIAccessMode(models.TextChoices):
|
|||
CACHED = "cached"
|
||||
|
||||
|
||||
class LDAPProvider(OutpostModel, Provider):
|
||||
class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
"""Allow applications to authenticate against authentik's users using LDAP."""
|
||||
|
||||
base_dn = models.TextField(
|
||||
|
|
|
@ -5,10 +5,16 @@ from django.utils.translation import gettext_lazy as _
|
|||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_SA, Group, PropertyMapping, Provider, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
BackchannelProvider,
|
||||
Group,
|
||||
PropertyMapping,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class SCIMProvider(Provider):
|
||||
class SCIMProvider(BackchannelProvider):
|
||||
"""SCIM 2.0 provider to create users and groups in external applications"""
|
||||
|
||||
exclude_users_service_account = models.BooleanField(default=False)
|
||||
|
|
|
@ -35,7 +35,7 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
|
|||
@CELERY_APP.task()
|
||||
def scim_sync_all():
|
||||
"""Run sync for all providers"""
|
||||
for provider in SCIMProvider.objects.all():
|
||||
for provider in SCIMProvider.objects.all(backchannel_application__isnull=False):
|
||||
scim_sync.delay(provider.pk)
|
||||
|
||||
|
||||
|
@ -96,6 +96,14 @@ def scim_sync_users(page: int, provider_pk: int):
|
|||
)
|
||||
except StopSync as exc:
|
||||
LOGGER.warning("Stopping sync", exc=exc)
|
||||
messages.append(
|
||||
_(
|
||||
"Stopping sync due to error: %(error)s"
|
||||
% {
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
)
|
||||
break
|
||||
return messages
|
||||
|
||||
|
@ -129,6 +137,14 @@ def scim_sync_group(page: int, provider_pk: int):
|
|||
)
|
||||
except StopSync as exc:
|
||||
LOGGER.warning("Stopping sync", exc=exc)
|
||||
messages.append(
|
||||
_(
|
||||
"Stopping sync due to error: %(error)s"
|
||||
% {
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
)
|
||||
break
|
||||
return messages
|
||||
|
||||
|
@ -141,7 +157,7 @@ def scim_signal_direct(model: str, pk: Any, raw_op: str):
|
|||
if not instance:
|
||||
return
|
||||
operation = PatchOp(raw_op)
|
||||
for provider in SCIMProvider.objects.all():
|
||||
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
|
||||
client = client_for_model(provider, instance)
|
||||
# Check if the object is allowed within the provider's restrictions
|
||||
queryset: Optional[QuerySet] = None
|
||||
|
@ -172,7 +188,7 @@ def scim_signal_m2m(group_pk: str, action: str, pk_set: list[int]):
|
|||
group = Group.objects.filter(pk=group_pk).first()
|
||||
if not group:
|
||||
return
|
||||
for provider in SCIMProvider.objects.all():
|
||||
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
|
||||
# Check if the object is allowed within the provider's restrictions
|
||||
queryset: QuerySet = provider.get_group_qs()
|
||||
# The queryset we get from the provider must include the instance we've got given
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
|||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.clients.base import SCIMClient
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
|
@ -18,6 +19,11 @@ class SCIMClientTests(TestCase):
|
|||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ from jsonschema import validate
|
|||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
|
||||
|
@ -26,6 +26,11 @@ class SCIMGroupTests(TestCase):
|
|||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.set(
|
||||
[SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ from guardian.shortcuts import get_anonymous_user
|
|||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
|
@ -15,6 +15,7 @@ class SCIMMembershipTests(TestCase):
|
|||
"""SCIM Membership tests"""
|
||||
|
||||
provider: SCIMProvider
|
||||
app: Application
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||
|
@ -30,6 +31,11 @@ class SCIMMembershipTests(TestCase):
|
|||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.set(
|
||||
[SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ from jsonschema import validate
|
|||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_sync
|
||||
|
@ -28,6 +28,11 @@ class SCIMUserTests(TestCase):
|
|||
token=generate_id(),
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Integrate ./manage.py test with pytest"""
|
||||
from argparse import ArgumentParser
|
||||
from unittest import TestCase
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -7,6 +8,8 @@ from authentik.lib.config import CONFIG
|
|||
from authentik.lib.sentry import sentry_init
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
|
||||
TestCase.maxDiff = None
|
||||
|
||||
|
||||
class PytestTestRunner: # pragma: no cover
|
||||
"""Runs pytest to discover and run tests."""
|
||||
|
|
93
schema.yml
93
schema.yml
|
@ -14293,6 +14293,10 @@ paths:
|
|||
name: application__isnull
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: backchannel_only
|
||||
schema:
|
||||
type: boolean
|
||||
- name: ordering
|
||||
required: false
|
||||
in: query
|
||||
|
@ -15831,6 +15835,11 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: backchannel_application
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: digest_algorithm
|
||||
schema:
|
||||
|
@ -15850,6 +15859,10 @@ paths:
|
|||
* `http://www.w3.org/2001/04/xmlenc#sha256` - SHA256
|
||||
* `http://www.w3.org/2001/04/xmldsig-more#sha384` - SHA384
|
||||
* `http://www.w3.org/2001/04/xmlenc#sha512` - SHA512
|
||||
- in: query
|
||||
name: is_backchannel
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: issuer
|
||||
schema:
|
||||
|
@ -26466,6 +26479,15 @@ components:
|
|||
allOf:
|
||||
- $ref: '#/components/schemas/Provider'
|
||||
readOnly: true
|
||||
backchannel_providers:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
backchannel_providers_obj:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Provider'
|
||||
readOnly: true
|
||||
launch_url:
|
||||
type: string
|
||||
nullable: true
|
||||
|
@ -26493,6 +26515,7 @@ components:
|
|||
group:
|
||||
type: string
|
||||
required:
|
||||
- backchannel_providers_obj
|
||||
- launch_url
|
||||
- meta_icon
|
||||
- name
|
||||
|
@ -26516,6 +26539,10 @@ components:
|
|||
provider:
|
||||
type: integer
|
||||
nullable: true
|
||||
backchannel_providers:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
open_in_new_tab:
|
||||
type: boolean
|
||||
description: Open launch URL in a new browser tab or window.
|
||||
|
@ -30550,6 +30577,8 @@ components:
|
|||
type: string
|
||||
application_slug:
|
||||
type: string
|
||||
description: Prioritise backchannel slug over direct application slug
|
||||
readOnly: true
|
||||
search_group:
|
||||
type: string
|
||||
format: uuid
|
||||
|
@ -30697,6 +30726,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -30751,6 +30788,8 @@ components:
|
|||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -31441,6 +31480,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -31522,6 +31569,8 @@ components:
|
|||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -35366,6 +35415,10 @@ components:
|
|||
provider:
|
||||
type: integer
|
||||
nullable: true
|
||||
backchannel_providers:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
open_in_new_tab:
|
||||
type: boolean
|
||||
description: Open launch URL in a new browser tab or window.
|
||||
|
@ -38362,6 +38415,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -38377,6 +38438,8 @@ components:
|
|||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -38594,6 +38657,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -38683,6 +38754,8 @@ components:
|
|||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- client_id
|
||||
- component
|
||||
|
@ -38850,6 +38923,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -38873,6 +38954,8 @@ components:
|
|||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -39177,6 +39260,14 @@ components:
|
|||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
|
@ -39274,6 +39365,8 @@ components:
|
|||
- acs_url
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@goauthentik/admin/applications/ProviderSelectModal";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import { first, groupBy } from "@goauthentik/common/utils";
|
||||
import { rootInterface } from "@goauthentik/elements/Base";
|
||||
|
@ -12,7 +13,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||
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 {
|
||||
|
@ -32,12 +33,16 @@ export class ApplicationForm extends ModelForm<Application, string> {
|
|||
slug: pk,
|
||||
});
|
||||
this.clearIcon = false;
|
||||
this.backchannelProviders = app.backchannelProvidersObj || [];
|
||||
return app;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
provider?: number;
|
||||
|
||||
@state()
|
||||
backchannelProviders: Provider[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
clearIcon = false;
|
||||
|
||||
|
@ -51,6 +56,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
|
|||
|
||||
async send(data: Application): Promise<Application | void> {
|
||||
let app: Application;
|
||||
data.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
|
||||
if (this.instance) {
|
||||
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({
|
||||
slug: this.instance.slug,
|
||||
|
@ -143,6 +149,47 @@ export class ApplicationForm extends ModelForm<Application, string> {
|
|||
${t`Select a provider that this application should use.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Backchannel providers`}
|
||||
name="backchannelProviders"
|
||||
>
|
||||
<div class="pf-c-input-group">
|
||||
<ak-provider-select-table
|
||||
?backchannelOnly=${true}
|
||||
.confirm=${(items: Provider[]) => {
|
||||
this.backchannelProviders = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ak-provider-select-table>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
${this.backchannelProviders.map((provider) => {
|
||||
return html`<ak-chip
|
||||
.removable=${true}
|
||||
value=${ifDefined(provider.pk)}
|
||||
@remove=${() => {
|
||||
const idx = this.backchannelProviders.indexOf(provider);
|
||||
this.backchannelProviders.splice(idx, 1);
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
${provider.name}
|
||||
</ak-chip>`;
|
||||
})}
|
||||
</ak-chip-group>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Select backchannel providers which augment the functionality of the main provider.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Policy engine mode`}
|
||||
?required=${true}
|
||||
|
|
|
@ -21,6 +21,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
@ -65,7 +66,17 @@ export class ApplicationViewPage extends AKElement {
|
|||
missingOutpost = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFBanner, PFPage, PFContent, PFButton, PFDescriptionList, PFGrid, PFCard];
|
||||
return [
|
||||
PFBase,
|
||||
PFList,
|
||||
PFBanner,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFButton,
|
||||
PFDescriptionList,
|
||||
PFGrid,
|
||||
PFCard,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -121,6 +132,35 @@ export class ApplicationViewPage extends AKElement {
|
|||
</dd>
|
||||
</div>`
|
||||
: html``}
|
||||
${(this.application.backchannelProvidersObj || []).length > 0
|
||||
? html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${t`Backchannel Providers`}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ul class="pf-c-list">
|
||||
${this.application.backchannelProvidersObj.map(
|
||||
(provider) => {
|
||||
return html`
|
||||
<li>
|
||||
<a
|
||||
href="#/core/providers/${provider.pk}"
|
||||
>
|
||||
${provider.name}
|
||||
(${provider.verboseName})
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TableModal } from "@goauthentik/elements/table/TableModal";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { Provider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-select-table")
|
||||
export class ProviderSelectModal extends TableModal<Provider> {
|
||||
checkbox = true;
|
||||
checkboxChip = true;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
backchannelOnly = false;
|
||||
|
||||
@property()
|
||||
confirm!: (selectedItems: Provider[]) => Promise<unknown>;
|
||||
|
||||
order = "name";
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Provider>> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersAllList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
backchannelOnly: this.backchannelOnly,
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [new TableColumn(t`Name`, "username"), new TableColumn(t`Type`)];
|
||||
}
|
||||
|
||||
row(item: Provider): TemplateResult[] {
|
||||
return [
|
||||
html`<div>
|
||||
<div>${item.name}</div>
|
||||
</div>`,
|
||||
html`${item.verboseName}`,
|
||||
];
|
||||
}
|
||||
|
||||
renderSelectedChip(item: Provider): TemplateResult {
|
||||
return html`${item.name}`;
|
||||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
${t`Select providers to add to application`}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
await this.confirm(this.selectedElements);
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-primary"
|
||||
>
|
||||
${t`Add`} </ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
}
|
|
@ -82,24 +82,29 @@ export class ProviderListPage extends TablePage<Provider> {
|
|||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Provider): TemplateResult[] {
|
||||
let app = html``;
|
||||
if (item.component === "ak-provider-scim-form") {
|
||||
app = html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
|
||||
${t`No application required.`}`;
|
||||
} else if (!item.assignedApplicationName) {
|
||||
app = html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
|
||||
${t`Warning: Provider not assigned to any application.`}`;
|
||||
} else {
|
||||
app = html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
|
||||
rowApp(item: Provider): TemplateResult {
|
||||
if (item.assignedApplicationName) {
|
||||
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
|
||||
${t`Assigned to application `}
|
||||
<a href="#/core/applications/${item.assignedApplicationSlug}"
|
||||
>${item.assignedApplicationName}</a
|
||||
>`;
|
||||
}
|
||||
if (item.assignedBackchannelApplicationName) {
|
||||
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
|
||||
${t`Assigned to application (backchannel) `}
|
||||
<a href="#/core/applications/${item.assignedBackchannelApplicationSlug}"
|
||||
>${item.assignedBackchannelApplicationName}</a
|
||||
>`;
|
||||
}
|
||||
return html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
|
||||
${t`Warning: Provider not assigned to any application.`}`;
|
||||
}
|
||||
|
||||
row(item: Provider): TemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
|
||||
app,
|
||||
this.rowApp(item),
|
||||
html`${item.verboseName}`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
|
|
|
@ -3,9 +3,9 @@ title: Applications
|
|||
slug: /applications
|
||||
---
|
||||
|
||||
Applications in authentik are the other half of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application.
|
||||
Applications in authentik are the other half of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application. Starting with authentik 2023.5, applications can use multiple providers, to augment the functionality of the main provider. For more information, see [Backchannel providers](#backchannel-providers).
|
||||
|
||||
Applications are used to configure and separate the authorization / access control and the appearance in the Library page.
|
||||
Applications are used to configure and separate the authorization / access control and the appearance in the _My applications_ page.
|
||||
|
||||
## Authorization
|
||||
|
||||
|
@ -54,3 +54,13 @@ Requires authentik 2022.3
|
|||
:::
|
||||
|
||||
To give users direct links to applications, you can now use an URL like `https://authentik.company/application/launch/<slug>/`. This will redirect the user directly if they're already logged in, and otherwise authenticate the user, and then forward them.
|
||||
|
||||
### Backchannel providers
|
||||
|
||||
:::info
|
||||
Requires authentik version 2023.5 or later.
|
||||
:::
|
||||
|
||||
Backchannel providers can augment the functionality of applications by using additional protocols. The main provider of an application provides the SSO protocol that is used for logging into the application. Then, additional backchannel providers can be used for protocols such as [SCIM](../providers/scim/index.md) and [LDAP](../providers/ldap/index.md) to provide directory syncing.
|
||||
|
||||
Access restrictions that are configured on an application apply to all of its backchannel providers.
|
||||
|
|
|
@ -53,7 +53,7 @@ settings:
|
|||
|
||||
### `settings.layout.type`
|
||||
|
||||
Which layout to use for the Library view. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
Which layout to use for the _My applications_ view. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
|
||||
### `settings.locale`
|
||||
|
||||
|
|
Reference in New Issue