diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index c3738e00b..d46be9528 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -47,6 +47,7 @@ from authentik.policies.reputation.api import ( ReputationPolicyViewSet, UserReputationViewSet, ) +from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet from authentik.providers.oauth2.api.scope import ScopeMappingViewSet from authentik.providers.oauth2.api.tokens import ( @@ -121,6 +122,7 @@ router.register( "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet ) router.register("outposts/proxy", ProxyOutpostConfigViewSet) +router.register("outposts/ldap", LDAPOutpostConfigViewSet) router.register("flows/instances", FlowViewSet) router.register("flows/bindings", FlowStageBindingViewSet) @@ -151,6 +153,7 @@ router.register("policies/reputation/ips", IPReputationViewSet) router.register("policies/reputation", ReputationPolicyViewSet) router.register("providers/all", ProviderViewSet) +router.register("providers/ldap", LDAPProviderViewSet) router.register("providers/proxy", ProxyProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/saml", SAMLProviderViewSet) diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index b43d26431..76d8710f8 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -91,6 +91,23 @@ class ApplicationViewSet(ModelViewSet): applications.append(application) return applications + @swagger_auto_schema( + responses={ + 204: "Access granted", + 403: "Access denied", + } + ) + @action(detail=True, methods=["GET"]) + # pylint: disable=unused-argument + def check_access(self, request: Request, slug: str) -> Response: + """Check access to a single application by slug""" + application = self.get_object() + engine = PolicyEngine(application, self.request.user, self.request) + engine.build() + if engine.passing: + return Response(status=204) + return Response(status=403) + @swagger_auto_schema( manual_parameters=[ openapi.Parameter( diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index 46e220855..de30d1ee4 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -1,7 +1,9 @@ """Groups API Viewset""" +from django.db.models.query import QuerySet from rest_framework.fields import JSONField from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet +from rest_framework_guardian.filters import ObjectPermissionsFilter from authentik.core.api.utils import is_dict from authentik.core.models import Group @@ -26,3 +28,16 @@ class GroupViewSet(ModelViewSet): search_fields = ["name", "is_superuser"] filterset_fields = ["name", "is_superuser"] ordering = ["name"] + + def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: + """Custom filter_queryset method which ignores guardian, but still supports sorting""" + for backend in list(self.filter_backends): + if backend == ObjectPermissionsFilter: + continue + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def filter_queryset(self, queryset): + if self.request.user.has_perm("authentik_core.view_group"): + return self._filter_queryset_for_list(queryset) + return super().filter_queryset(queryset) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 6f23700d5..c4d1f6bb9 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -1,6 +1,7 @@ """User API Views""" from json import loads +from django.db.models.query import QuerySet from django.http.response import Http404 from django.urls import reverse_lazy from django.utils.http import urlencode @@ -12,11 +13,18 @@ from rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import BooleanField, ModelSerializer, ValidationError +from rest_framework.serializers import ( + BooleanField, + ListSerializer, + ModelSerializer, + ValidationError, +) from rest_framework.viewsets import ModelViewSet +from rest_framework_guardian.filters import ObjectPermissionsFilter from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.api.decorators import permission_required +from authentik.core.api.groups import GroupSerializer from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.middleware import ( SESSION_IMPERSONATE_ORIGINAL_USER, @@ -33,6 +41,7 @@ class UserSerializer(ModelSerializer): is_superuser = BooleanField(read_only=True) avatar = CharField(read_only=True) attributes = JSONField(validators=[is_dict], required=False) + groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups") class Meta: @@ -44,6 +53,7 @@ class UserSerializer(ModelSerializer): "is_active", "last_login", "is_superuser", + "groups", "email", "avatar", "attributes", @@ -177,3 +187,16 @@ class UserViewSet(ModelViewSet): reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" ) return Response({"link": link}) + + def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: + """Custom filter_queryset method which ignores guardian, but still supports sorting""" + for backend in list(self.filter_backends): + if backend == ObjectPermissionsFilter: + continue + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def filter_queryset(self, queryset): + if self.request.user.has_perm("authentik_core.view_group"): + return self._filter_queryset_for_list(queryset) + return super().filter_queryset(queryset) diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py new file mode 100644 index 000000000..40a554e2e --- /dev/null +++ b/authentik/core/tests/test_applications_api.py @@ -0,0 +1,125 @@ +"""Test Applications API""" +from django.urls import reverse +from django.utils.encoding import force_str +from rest_framework.test import APITestCase + +from authentik.core.models import Application, User +from authentik.policies.dummy.models import DummyPolicy +from authentik.policies.models import PolicyBinding + + +class TestApplicationsAPI(APITestCase): + """Test applications API""" + + def setUp(self) -> None: + self.user = User.objects.get(username="akadmin") + self.allowed = Application.objects.create(name="allowed", slug="allowed") + self.denied = Application.objects.create(name="denied", slug="denied") + PolicyBinding.objects.create( + target=self.denied, + policy=DummyPolicy.objects.create( + name="deny", result=False, wait_min=1, wait_max=2 + ), + order=0, + ) + + def test_check_access(self): + """Test check_access operation """ + self.client.force_login(self.user) + response = self.client.get( + reverse( + "authentik_api:application-check-access", + kwargs={"slug": self.allowed.slug}, + ) + ) + self.assertEqual(response.status_code, 204) + response = self.client.get( + reverse( + "authentik_api:application-check-access", + kwargs={"slug": self.denied.slug}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_list(self): + """Test list operation without superuser_full_list""" + self.client.force_login(self.user) + response = self.client.get(reverse("authentik_api:application-list")) + self.assertJSONEqual( + force_str(response.content), + { + "pagination": { + "next": 0, + "previous": 0, + "count": 2, + "current": 1, + "total_pages": 1, + "start_index": 1, + "end_index": 2, + }, + "results": [ + { + "pk": str(self.allowed.pk), + "name": "allowed", + "slug": "allowed", + "provider": None, + "provider_obj": None, + "launch_url": None, + "meta_launch_url": "", + "meta_icon": None, + "meta_description": "", + "meta_publisher": "", + "policy_engine_mode": "any", + }, + ], + }, + ) + + def test_list_superuser_full_list(self): + """Test list operation with superuser_full_list""" + self.client.force_login(self.user) + response = self.client.get( + reverse("authentik_api:application-list") + "?superuser_full_list=true" + ) + self.assertJSONEqual( + force_str(response.content), + { + "pagination": { + "next": 0, + "previous": 0, + "count": 2, + "current": 1, + "total_pages": 1, + "start_index": 1, + "end_index": 2, + }, + "results": [ + { + "pk": str(self.allowed.pk), + "name": "allowed", + "slug": "allowed", + "provider": None, + "provider_obj": None, + "launch_url": None, + "meta_launch_url": "", + "meta_icon": None, + "meta_description": "", + "meta_publisher": "", + "policy_engine_mode": "any", + }, + { + "launch_url": None, + "meta_description": "", + "meta_icon": None, + "meta_launch_url": "", + "meta_publisher": "", + "name": "denied", + "pk": str(self.denied.pk), + "policy_engine_mode": "any", + "provider": None, + "provider_obj": None, + "slug": "denied", + }, + ], + }, + ) diff --git a/authentik/events/api/notification.py b/authentik/events/api/notification.py index 70066ae0e..df012cf67 100644 --- a/authentik/events/api/notification.py +++ b/authentik/events/api/notification.py @@ -50,4 +50,4 @@ class NotificationViewSet( def get_queryset(self): user = self.request.user if self.request else get_anonymous_user() - return Notification.objects.filter(user=user) + return Notification.objects.filter(user=user.pk) diff --git a/authentik/lib/config.py b/authentik/lib/config.py index 033d2431f..c71d9a573 100644 --- a/authentik/lib/config.py +++ b/authentik/lib/config.py @@ -86,6 +86,12 @@ class ConfigLoader: url = urlparse(value) if url.scheme == "env": value = os.getenv(url.netloc, url.query) + if url.scheme == "file": + try: + with open(url.netloc, "r") as _file: + value = _file.read() + except OSError: + self._log("error", f"Failed to read config value from {url.netloc}") return value def update_from_file(self, path: str): @@ -163,6 +169,7 @@ class ConfigLoader: # Walk each component of the path path_parts = path.split(sep) for comp in path_parts[:-1]: + # pyright: reportGeneralTypeIssues=false if comp not in root: root[comp] = {} root = root.get(comp) diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index dbe2595af..af0941eda 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -24,6 +24,7 @@ class OutpostSerializer(ModelSerializer): fields = [ "pk", "name", + "type", "providers", "providers_obj", "service_connection", diff --git a/authentik/outposts/migrations/0016_alter_outpost_type.py b/authentik/outposts/migrations/0016_alter_outpost_type.py new file mode 100644 index 000000000..966b86997 --- /dev/null +++ b/authentik/outposts/migrations/0016_alter_outpost_type.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2021-04-26 09:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_outposts", "0015_auto_20201224_1206"), + ] + + operations = [ + migrations.AlterField( + model_name="outpost", + name="type", + field=models.TextField( + choices=[("proxy", "Proxy"), ("ldap", "Ldap")], default="proxy" + ), + ), + ] diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index d8e75cdf4..2cf24c6a1 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -5,6 +5,7 @@ from typing import Iterable, Optional, Union from uuid import uuid4 from dacite import from_dict +from django.contrib.auth.models import Permission from django.core.cache import cache from django.db import models, transaction from django.db.models.base import Model @@ -64,7 +65,7 @@ class OutpostConfig: class OutpostModel(Model): """Base model for providers that need more objects than just themselves""" - def get_required_objects(self) -> Iterable[models.Model]: + def get_required_objects(self) -> Iterable[Union[models.Model, str]]: """Return a list of all required objects""" return [self] @@ -77,6 +78,7 @@ class OutpostType(models.TextChoices): """Outpost types, currently only the reverse proxy is available""" PROXY = "proxy" + LDAP = "ldap" def default_outpost_config(host: Optional[str] = None): @@ -334,9 +336,26 @@ class Outpost(models.Model): # the ones the user needs with transaction.atomic(): UserObjectPermission.objects.filter(user=user).delete() - for model in self.get_required_objects(): - code_name = f"{model._meta.app_label}.view_{model._meta.model_name}" - assign_perm(code_name, user, model) + user.user_permissions.clear() + for model_or_perm in self.get_required_objects(): + if isinstance(model_or_perm, models.Model): + model_or_perm: models.Model + code_name = ( + f"{model_or_perm._meta.app_label}." + f"view_{model_or_perm._meta.model_name}" + ) + assign_perm(code_name, user, model_or_perm) + else: + app_label, perm = model_or_perm.split(".") + permission = Permission.objects.filter( + codename=perm, + content_type__app_label=app_label, + ) + if not permission.exists(): + LOGGER.warning("permission doesn't exist", perm=model_or_perm) + continue + user.user_permissions.add(permission.first()) + LOGGER.debug("Updated service account's permissions") return user @property @@ -359,9 +378,9 @@ class Outpost(models.Model): managed=f"goauthentik.io/outpost/{self.token_identifier}", ) - def get_required_objects(self) -> Iterable[models.Model]: + def get_required_objects(self) -> Iterable[Union[models.Model, str]]: """Get an iterator of all objects the user needs read access to""" - objects = [self] + objects: list[Union[models.Model, str]] = [self] for provider in ( Provider.objects.filter(outpost=self).select_related().select_subclasses() ): diff --git a/authentik/outposts/tasks.py b/authentik/outposts/tasks.py index ece820add..aa5d569ce 100644 --- a/authentik/outposts/tasks.py +++ b/authentik/outposts/tasks.py @@ -3,7 +3,7 @@ from os import R_OK, access from os.path import expanduser from pathlib import Path from socket import gethostname -from typing import Any +from typing import Any, Optional from urllib.parse import urlparse import yaml @@ -19,7 +19,7 @@ from structlog.stdlib import get_logger from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.lib.utils.reflection import path_to_class -from authentik.outposts.controllers.base import ControllerException +from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.models import ( DockerServiceConnection, KubernetesServiceConnection, @@ -29,6 +29,8 @@ from authentik.outposts.models import ( OutpostState, OutpostType, ) +from authentik.providers.ldap.controllers.docker import LDAPDockerController +from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController from authentik.providers.proxy.controllers.docker import ProxyDockerController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.root.celery import CELERY_APP @@ -36,6 +38,24 @@ from authentik.root.celery import CELERY_APP LOGGER = get_logger() +def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]: + """Get a controller for the outpost, when a service connection is defined""" + if not outpost.service_connection: + return None + service_connection = outpost.service_connection + if outpost.type == OutpostType.PROXY: + if isinstance(service_connection, DockerServiceConnection): + return ProxyDockerController(outpost, service_connection) + if isinstance(service_connection, KubernetesServiceConnection): + return ProxyKubernetesController(outpost, service_connection) + if outpost.type == OutpostType.LDAP: + if isinstance(service_connection, DockerServiceConnection): + return LDAPDockerController(outpost, service_connection) + if isinstance(service_connection, KubernetesServiceConnection): + return LDAPKubernetesController(outpost, service_connection) + return None + + @CELERY_APP.task() def outpost_controller_all(): """Launch Controller for all Outposts which support it""" @@ -76,16 +96,10 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str): outpost: Outpost = Outpost.objects.get(pk=outpost_pk) self.set_uid(slugify(outpost.name)) try: - if not outpost.service_connection: + controller = controller_for_outpost(outpost) + if not controller: return - if outpost.type == OutpostType.PROXY: - service_connection = outpost.service_connection - if isinstance(service_connection, DockerServiceConnection): - logs = ProxyDockerController(outpost, service_connection).up_with_logs() - if isinstance(service_connection, KubernetesServiceConnection): - logs = ProxyKubernetesController( - outpost, service_connection - ).up_with_logs() + logs = controller.up_with_logs() LOGGER.debug("---------------Outpost Controller logs starting----------------") for log in logs: LOGGER.debug(log) @@ -100,12 +114,10 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str): def outpost_pre_delete(outpost_pk: str): """Delete outpost objects before deleting the DB Object""" outpost = Outpost.objects.get(pk=outpost_pk) - if outpost.type == OutpostType.PROXY: - service_connection = outpost.service_connection - if isinstance(service_connection, DockerServiceConnection): - ProxyDockerController(outpost, service_connection).down() - if isinstance(service_connection, KubernetesServiceConnection): - ProxyKubernetesController(outpost, service_connection).down() + controller = controller_for_outpost(outpost) + if not controller: + return + controller.down() @CELERY_APP.task(bind=True, base=MonitoredTask) diff --git a/authentik/providers/ldap/__init__.py b/authentik/providers/ldap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/ldap/api.py b/authentik/providers/ldap/api.py new file mode 100644 index 000000000..d9900f3c5 --- /dev/null +++ b/authentik/providers/ldap/api.py @@ -0,0 +1,54 @@ +"""LDAPProvider API Views""" +from rest_framework.fields import CharField +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet + +from authentik.core.api.providers import ProviderSerializer +from authentik.providers.ldap.models import LDAPProvider + + +class LDAPProviderSerializer(ProviderSerializer): + """LDAPProvider Serializer""" + + class Meta: + + model = LDAPProvider + fields = ProviderSerializer.Meta.fields + [ + "base_dn", + "search_group", + ] + + +class LDAPProviderViewSet(ModelViewSet): + """LDAPProvider Viewset""" + + queryset = LDAPProvider.objects.all() + serializer_class = LDAPProviderSerializer + ordering = ["name"] + + +class LDAPOutpostConfigSerializer(ModelSerializer): + """LDAPProvider Serializer""" + + application_slug = CharField(source="application.slug") + bind_flow_slug = CharField(source="authorization_flow.slug") + + class Meta: + + model = LDAPProvider + fields = [ + "pk", + "name", + "base_dn", + "bind_flow_slug", + "application_slug", + "search_group", + ] + + +class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet): + """LDAPProvider Viewset""" + + queryset = LDAPProvider.objects.filter(application__isnull=False) + serializer_class = LDAPOutpostConfigSerializer + ordering = ["name"] diff --git a/authentik/providers/ldap/apps.py b/authentik/providers/ldap/apps.py new file mode 100644 index 000000000..7adc551ff --- /dev/null +++ b/authentik/providers/ldap/apps.py @@ -0,0 +1,10 @@ +"""authentik ldap provider app config""" +from django.apps import AppConfig + + +class AuthentikProviderLDAPConfig(AppConfig): + """authentik ldap provider app config""" + + name = "authentik.providers.ldap" + label = "authentik_providers_ldap" + verbose_name = "authentik Providers.LDAP" diff --git a/authentik/providers/ldap/controllers/__init__.py b/authentik/providers/ldap/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/ldap/controllers/docker.py b/authentik/providers/ldap/controllers/docker.py new file mode 100644 index 000000000..f2d93e93d --- /dev/null +++ b/authentik/providers/ldap/controllers/docker.py @@ -0,0 +1,14 @@ +"""LDAP Provider Docker Contoller""" +from authentik.outposts.controllers.base import DeploymentPort +from authentik.outposts.controllers.docker import DockerController +from authentik.outposts.models import DockerServiceConnection, Outpost + + +class LDAPDockerController(DockerController): + """LDAP Provider Docker Contoller""" + + def __init__(self, outpost: Outpost, connection: DockerServiceConnection): + super().__init__(outpost, connection) + self.deployment_ports = [ + DeploymentPort(3389, "ldap", "tcp"), + ] diff --git a/authentik/providers/ldap/controllers/kubernetes.py b/authentik/providers/ldap/controllers/kubernetes.py new file mode 100644 index 000000000..924f9bf9b --- /dev/null +++ b/authentik/providers/ldap/controllers/kubernetes.py @@ -0,0 +1,14 @@ +"""LDAP Provider Kubernetes Contoller""" +from authentik.outposts.controllers.base import DeploymentPort +from authentik.outposts.controllers.kubernetes import KubernetesController +from authentik.outposts.models import KubernetesServiceConnection, Outpost + + +class LDAPKubernetesController(KubernetesController): + """LDAP Provider Kubernetes Contoller""" + + def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection): + super().__init__(outpost, connection) + self.deployment_ports = [ + DeploymentPort(3389, "ldap", "tcp"), + ] diff --git a/authentik/providers/ldap/migrations/0001_initial.py b/authentik/providers/ldap/migrations/0001_initial.py new file mode 100644 index 000000000..eaf490403 --- /dev/null +++ b/authentik/providers/ldap/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2 on 2021-04-26 12:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_core", "0019_source_managed"), + ] + + operations = [ + migrations.CreateModel( + name="LDAPProvider", + fields=[ + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_core.provider", + ), + ), + ( + "base_dn", + models.TextField( + default="DC=ldap,DC=goauthentik,DC=io", + help_text="DN under which objects are accessible.", + ), + ), + ], + options={ + "verbose_name": "LDAP Provider", + "verbose_name_plural": "LDAP Providers", + }, + bases=("authentik_core.provider", models.Model), + ), + ] diff --git a/authentik/providers/ldap/migrations/0002_ldapprovider_search_group.py b/authentik/providers/ldap/migrations/0002_ldapprovider_search_group.py new file mode 100644 index 000000000..afa4aa4a0 --- /dev/null +++ b/authentik/providers/ldap/migrations/0002_ldapprovider_search_group.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2 on 2021-04-26 19:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_core", "0019_source_managed"), + ("authentik_providers_ldap", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="ldapprovider", + name="search_group", + field=models.ForeignKey( + default=None, + help_text="Users in this group can do search queries. If not set, every user can execute search queries.", + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="authentik_core.group", + ), + ), + ] diff --git a/authentik/providers/ldap/migrations/__init__.py b/authentik/providers/ldap/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/providers/ldap/models.py b/authentik/providers/ldap/models.py new file mode 100644 index 000000000..fe970080b --- /dev/null +++ b/authentik/providers/ldap/models.py @@ -0,0 +1,55 @@ +"""LDAP Provider""" +from typing import Iterable, Optional, Type, Union + +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.outposts.models import OutpostModel + + +class LDAPProvider(OutpostModel, Provider): + """Allow applications to authenticate against authentik's users using LDAP.""" + + base_dn = models.TextField( + default="DC=ldap,DC=goauthentik,DC=io", + help_text=_("DN under which objects are accessible."), + ) + + search_group = models.ForeignKey( + Group, + null=True, + default=None, + on_delete=models.SET_DEFAULT, + help_text=_( + "Users in this group can do search queries. " + "If not set, every user can execute search queries." + ), + ) + + @property + def launch_url(self) -> Optional[str]: + """LDAP never has a launch URL""" + return None + + @property + def component(self) -> str: + return "ak-provider-ldap-form" + + @property + def serializer(self) -> Type[Serializer]: + from authentik.providers.ldap.api import LDAPProviderSerializer + + return LDAPProviderSerializer + + def __str__(self): + return f"LDAP Provider {self.name}" + + def get_required_objects(self) -> Iterable[Union[models.Model, str]]: + return [self, "authentik_core.view_user", "authentik_core.view_group"] + + class Meta: + + verbose_name = _("LDAP Provider") + verbose_name_plural = _("LDAP Providers") diff --git a/authentik/providers/oauth2/api/tokens.py b/authentik/providers/oauth2/api/tokens.py index f7e2ea3f2..fb00a044b 100644 --- a/authentik/providers/oauth2/api/tokens.py +++ b/authentik/providers/oauth2/api/tokens.py @@ -42,7 +42,7 @@ class AuthorizationCodeViewSet( user = self.request.user if self.request else get_anonymous_user() if user.is_superuser: return super().get_queryset() - return super().get_queryset().filter(user=user) + return super().get_queryset().filter(user=user.pk) class RefreshTokenViewSet( @@ -62,4 +62,4 @@ class RefreshTokenViewSet( user = self.request.user if self.request else get_anonymous_user() if user.is_superuser: return super().get_queryset() - return super().get_queryset().filter(user=user) + return super().get_queryset().filter(user=user.pk) diff --git a/authentik/providers/oauth2/apps.py b/authentik/providers/oauth2/apps.py index a23e33339..59e9fd23a 100644 --- a/authentik/providers/oauth2/apps.py +++ b/authentik/providers/oauth2/apps.py @@ -1,11 +1,11 @@ -"""authentik auth oauth provider app config""" +"""authentik oauth provider app config""" from importlib import import_module from django.apps import AppConfig class AuthentikProviderOAuth2Config(AppConfig): - """authentik auth oauth provider app config""" + """authentik oauth provider app config""" name = "authentik.providers.oauth2" label = "authentik_providers_oauth2" diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py index ed4df13ea..576b03a1c 100644 --- a/authentik/providers/proxy/models.py +++ b/authentik/providers/proxy/models.py @@ -1,7 +1,7 @@ """authentik proxy models""" import string from random import SystemRandom -from typing import Iterable, Optional, Type +from typing import Iterable, Optional, Type, Union from urllib.parse import urljoin from django.db import models @@ -147,7 +147,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider): def __str__(self): return f"Proxy Provider {self.name}" - def get_required_objects(self) -> Iterable[models.Model]: + def get_required_objects(self) -> Iterable[Union[models.Model, str]]: required_models = [self] if self.certificate is not None: required_models.append(self.certificate) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 4fa9d6355..344ce3e86 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -102,6 +102,7 @@ INSTALLED_APPS = [ "authentik.policies.password", "authentik.policies.reputation", "authentik.providers.proxy", + "authentik.providers.ldap", "authentik.providers.oauth2", "authentik.providers.saml", "authentik.recovery", diff --git a/authentik/sources/oauth/api/source_connection.py b/authentik/sources/oauth/api/source_connection.py index 763608150..e602ca96e 100644 --- a/authentik/sources/oauth/api/source_connection.py +++ b/authentik/sources/oauth/api/source_connection.py @@ -30,4 +30,4 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet): user = self.request.user if self.request else get_anonymous_user() if user.is_superuser: return super().get_queryset() - return super().get_queryset().filter(user=user) + return super().get_queryset().filter(user=user.pk) diff --git a/authentik/stages/authenticator_static/api.py b/authentik/stages/authenticator_static/api.py index a7eeaee88..4b59964fa 100644 --- a/authentik/stages/authenticator_static/api.py +++ b/authentik/stages/authenticator_static/api.py @@ -46,7 +46,7 @@ class StaticDeviceViewSet(ModelViewSet): def get_queryset(self): user = self.request.user if self.request else get_anonymous_user() - return StaticDevice.objects.filter(user=user) + return StaticDevice.objects.filter(user=user.pk) class StaticAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/authenticator_totp/api.py b/authentik/stages/authenticator_totp/api.py index f6aa67e57..3b2cbde03 100644 --- a/authentik/stages/authenticator_totp/api.py +++ b/authentik/stages/authenticator_totp/api.py @@ -49,7 +49,7 @@ class TOTPDeviceViewSet(ModelViewSet): def get_queryset(self): user = self.request.user if self.request else get_anonymous_user() - return TOTPDevice.objects.filter(user=user) + return TOTPDevice.objects.filter(user=user.pk) class TOTPAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/authenticator_webauthn/api.py b/authentik/stages/authenticator_webauthn/api.py index 2ea373efe..0ecb08b28 100644 --- a/authentik/stages/authenticator_webauthn/api.py +++ b/authentik/stages/authenticator_webauthn/api.py @@ -48,7 +48,7 @@ class WebAuthnDeviceViewSet(ModelViewSet): def get_queryset(self): user = self.request.user if self.request else get_anonymous_user() - return WebAuthnDevice.objects.filter(user=user) + return WebAuthnDevice.objects.filter(user=user.pk) class WebAuthnAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/consent/api.py b/authentik/stages/consent/api.py index ffe600f3b..501b4f008 100644 --- a/authentik/stages/consent/api.py +++ b/authentik/stages/consent/api.py @@ -54,4 +54,4 @@ class UserConsentViewSet( user = self.request.user if self.request else get_anonymous_user() if user.is_superuser: return super().get_queryset() - return super().get_queryset().filter(user=user) + return super().get_queryset().filter(user=user.pk) diff --git a/authentik/stages/email/tasks.py b/authentik/stages/email/tasks.py index 517ae1a4d..8aafdc560 100644 --- a/authentik/stages/email/tasks.py +++ b/authentik/stages/email/tasks.py @@ -51,6 +51,7 @@ def send_mail( try: backend = stage.backend except ValueError as exc: + # pyright: reportGeneralTypeIssues=false LOGGER.warning(exc) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) return diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ecc29ab41..bce0bd139 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -100,7 +100,7 @@ stages: versionSpec: '3.9' - task: CmdLine@2 inputs: - script: npm install -g pyright@1.1.109 + script: npm install -g pyright@1.1.136 - task: CmdLine@2 inputs: script: | @@ -262,6 +262,9 @@ stages: - task: UsePythonVersion@0 inputs: versionSpec: '3.9' + - task: NodeTool@0 + inputs: + versionSpec: '16.x' - task: DockerCompose@0 displayName: Run services inputs: @@ -379,7 +382,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: '14.x' + versionSpec: '16.x' displayName: 'Install Node.js' - task: CmdLine@2 inputs: diff --git a/outpost/azure-pipelines.yml b/outpost/azure-pipelines.yml index 4f1cf7901..96a8d71a0 100644 --- a/outpost/azure-pipelines.yml +++ b/outpost/azure-pipelines.yml @@ -51,9 +51,9 @@ stages: script: | docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.39.0 golangci-lint run -v --timeout 200s workingDirectory: 'outpost/' - - stage: proxy_build_go + - stage: build_go jobs: - - job: build_go + - job: proxy_build_go pool: vmImage: 'ubuntu-latest' steps: @@ -70,9 +70,26 @@ stages: command: 'build' arguments: './cmd/proxy' workingDirectory: 'outpost/' - - stage: proxy_build_docker + - job: ldap_build_go + pool: + vmImage: 'ubuntu-latest' + steps: + - task: GoTool@0 + inputs: + version: '1.16.3' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'go_swagger_client' + path: "outpost/pkg/" + - task: Go@0 + inputs: + command: 'build' + arguments: './cmd/ldap' + workingDirectory: 'outpost/' + - stage: build_docker jobs: - - job: build + - job: proxy_build_docker pool: vmImage: 'ubuntu-latest' steps: @@ -97,3 +114,28 @@ stages: Dockerfile: 'outpost/proxy.Dockerfile' buildContext: 'outpost/' tags: "gh-$(branchName)" + - job: ldap_build_docker + pool: + vmImage: 'ubuntu-latest' + steps: + - task: GoTool@0 + inputs: + version: '1.16.3' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'go_swagger_client' + path: "outpost/pkg/" + - task: Bash@3 + inputs: + targetType: 'inline' + script: | + python ./scripts/az_do_set_branch.py + - task: Docker@2 + inputs: + containerRegistry: 'beryjuorg-harbor' + repository: 'authentik/outpost-ldap' + command: 'buildAndPush' + Dockerfile: 'outpost/ldap.Dockerfile' + buildContext: 'outpost/' + tags: "gh-$(branchName)" diff --git a/outpost/cmd/ldap/server.go b/outpost/cmd/ldap/server.go new file mode 100644 index 000000000..e78924e93 --- /dev/null +++ b/outpost/cmd/ldap/server.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "math/rand" + "net/url" + "os" + "os/signal" + "time" + + log "github.com/sirupsen/logrus" + + "goauthentik.io/outpost/pkg/ak" + "goauthentik.io/outpost/pkg/ldap" +) + +const helpMessage = `authentik ldap + +Required environment variables: +- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company") +- AUTHENTIK_TOKEN: Token to authenticate with +- AUTHENTIK_INSECURE: Skip SSL Certificate verification` + +func main() { + log.SetLevel(log.DebugLevel) + pbURL, found := os.LookupEnv("AUTHENTIK_HOST") + if !found { + fmt.Println("env AUTHENTIK_HOST not set!") + fmt.Println(helpMessage) + os.Exit(1) + } + pbToken, found := os.LookupEnv("AUTHENTIK_TOKEN") + if !found { + fmt.Println("env AUTHENTIK_TOKEN not set!") + fmt.Println(helpMessage) + os.Exit(1) + } + + pbURLActual, err := url.Parse(pbURL) + if err != nil { + fmt.Println(err) + fmt.Println(helpMessage) + os.Exit(1) + } + + rand.Seed(time.Now().UnixNano()) + + ac := ak.NewAPIController(*pbURLActual, pbToken) + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + ac.Server = ldap.NewServer(ac) + + err = ac.Start() + if err != nil { + log.WithError(err).Panic("Failed to run server") + } + + for { + <-interrupt + ac.Shutdown() + os.Exit(0) + } +} diff --git a/outpost/go.mod b/outpost/go.mod index 3d83c99e6..7d7fcc00e 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -5,7 +5,9 @@ go 1.14 require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/coreos/go-oidc v2.2.1+incompatible + github.com/felixge/httpsnoop v1.0.2 // indirect github.com/getsentry/sentry-go v0.10.0 + github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-openapi/analysis v0.20.1 // indirect github.com/go-openapi/errors v0.20.0 github.com/go-openapi/runtime v0.19.28 @@ -13,6 +15,7 @@ require ( github.com/go-openapi/swag v0.19.15 github.com/go-openapi/validate v0.20.2 github.com/go-redis/redis/v7 v7.4.0 // indirect + github.com/go-swagger/go-swagger v0.27.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gorilla/websocket v1.4.2 github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a @@ -20,6 +23,8 @@ require ( github.com/kr/pretty v0.2.1 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect + github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 // indirect github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc github.com/pelletier/go-toml v1.9.0 // indirect github.com/pkg/errors v0.9.1 @@ -32,9 +37,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.1 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect - golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d // indirect + golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect + golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect diff --git a/outpost/go.sum b/outpost/go.sum index 1b567c857..d08d4491c 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -34,6 +34,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -102,6 +104,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -123,6 +126,9 @@ github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHj github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -136,6 +142,8 @@ github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NB github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -143,6 +151,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ= +github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= @@ -167,6 +177,8 @@ github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpX github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.0 h1:Sxpo9PjEHDzhs3FbnGNonvDgWcMW2U7wGTcDDSFSceM= github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= +github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= @@ -196,6 +208,7 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.27/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/runtime v0.19.28 h1:9lYu6axek8LJrVkMVViVirRcpoaCxXX7+sSvmizGVnA= github.com/go-openapi/runtime v0.19.28/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= @@ -246,6 +259,9 @@ github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRf github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-swagger/go-swagger v0.27.0 h1:K7+nkBuf4oS1jTBrdvWqYFpqD69V5CN8HamZzCDDhAI= +github.com/go-swagger/go-swagger v0.27.0/go.mod h1:WodZVysInJilkW7e6IRw+dZGp5yW6rlMFZ4cb+THl9A= +github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -341,6 +357,8 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -379,6 +397,8 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -483,6 +503,10 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s= +github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA= +github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8= +github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc h1:jf/4meI7lkRwGoiD7Ex/ns0BekEPKZ8nsB3u2oLhLGM= @@ -506,6 +530,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0= github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= @@ -540,6 +565,7 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= @@ -566,6 +592,7 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -574,6 +601,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -591,6 +619,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -625,6 +655,7 @@ github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= @@ -664,6 +695,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -697,6 +729,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -737,13 +771,15 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -760,6 +796,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -810,9 +847,12 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 h1:kHSDPqCtsHZOg0nVylfTo20DDhE9gG4Y0jn7hKQ0QAM= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -879,6 +919,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/outpost/ldap.Dockerfile b/outpost/ldap.Dockerfile new file mode 100644 index 000000000..a440468c4 --- /dev/null +++ b/outpost/ldap.Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.16.3 AS builder + +WORKDIR /work + +COPY . . + +RUN go build -o /work/ldap ./cmd/ldap + +FROM gcr.io/distroless/base-debian10:debug + +COPY --from=builder /work/ldap / + +ENTRYPOINT ["/ldap"] diff --git a/outpost/pkg/ak/global.go b/outpost/pkg/ak/global.go index f6678f6b9..b9512fc90 100644 --- a/outpost/pkg/ak/global.go +++ b/outpost/pkg/ak/global.go @@ -31,7 +31,7 @@ func doGlobalSetup(config map[string]interface{}) { default: log.SetLevel(log.DebugLevel) } - log.WithField("version", pkg.VERSION).Info("Starting authentik proxy") + log.WithField("version", pkg.VERSION).Info("Starting authentik outpost") var dsn string if config[ConfigErrorReportingEnabled].(bool) { diff --git a/outpost/pkg/ldap/api.go b/outpost/pkg/ldap/api.go new file mode 100644 index 000000000..3b7717651 --- /dev/null +++ b/outpost/pkg/ldap/api.go @@ -0,0 +1,50 @@ +package ldap + +import ( + "errors" + "fmt" + "strings" + + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" + "goauthentik.io/outpost/pkg/client/outposts" +) + +func (ls *LDAPServer) Refresh() error { + outposts, err := ls.ac.Client.Outposts.OutpostsLdapList(outposts.NewOutpostsLdapListParams(), ls.ac.Auth) + if err != nil { + return err + } + if len(outposts.Payload.Results) < 1 { + return errors.New("no ldap provider defined") + } + providers := make([]*ProviderInstance, len(outposts.Payload.Results)) + for idx, provider := range outposts.Payload.Results { + userDN := strings.ToLower(fmt.Sprintf("cn=users,%s", provider.BaseDn)) + groupDN := strings.ToLower(fmt.Sprintf("cn=groups,%s", provider.BaseDn)) + providers[idx] = &ProviderInstance{ + BaseDN: provider.BaseDn, + GroupDN: groupDN, + UserDN: userDN, + appSlug: *provider.ApplicationSlug, + flowSlug: *provider.BindFlowSlug, + searchAllowedGroups: []*strfmt.UUID{provider.SearchGroup}, + s: ls, + log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name), + } + } + ls.providers = providers + ls.log.Info("Update providers") + return nil +} + +func (ls *LDAPServer) Start() error { + listen := "0.0.0.0:3389" + log.Debugf("Listening on %s", listen) + err := ls.s.ListenAndServe(listen) + if err != nil { + ls.log.Errorf("LDAP Server Failed: %s", err.Error()) + return err + } + return nil +} diff --git a/outpost/pkg/ldap/bind.go b/outpost/pkg/ldap/bind.go new file mode 100644 index 000000000..8ebdabcd6 --- /dev/null +++ b/outpost/pkg/ldap/bind.go @@ -0,0 +1,19 @@ +package ldap + +import ( + "net" + + "github.com/nmcclain/ldap" +) + +func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { + ls.log.WithField("boundDN", bindDN).Info("bind") + for _, instance := range ls.providers { + username, err := instance.getUsername(bindDN) + if err == nil { + return instance.Bind(username, bindPW, conn) + } + } + ls.log.WithField("boundDN", bindDN).WithField("request", "bind").Warning("No provider found for request") + return ldap.LDAPResultOperationsError, nil +} diff --git a/outpost/pkg/ldap/instance_bind.go b/outpost/pkg/ldap/instance_bind.go new file mode 100644 index 000000000..ea1f7b7a3 --- /dev/null +++ b/outpost/pkg/ldap/instance_bind.go @@ -0,0 +1,174 @@ +package ldap + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/cookiejar" + "strings" + "time" + + goldap "github.com/go-ldap/ldap/v3" + httptransport "github.com/go-openapi/runtime/client" + "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/client/core" + "goauthentik.io/outpost/pkg/client/flows" + "goauthentik.io/outpost/pkg/models" +) + +const ContextUserKey = "ak_user" + +type UIDResponse struct { + UIDFIeld string `json:"uid_field"` +} + +type PasswordResponse struct { + Password string `json:"password"` +} + +func (pi *ProviderInstance) getUsername(dn string) (string, error) { + if !strings.HasSuffix(dn, pi.BaseDN) { + return "", errors.New("invalid base DN") + } + dns, err := goldap.ParseDN(dn) + if err != nil { + return "", err + } + for _, part := range dns.RDNs { + for _, attribute := range part.Attributes { + if attribute.Type == "DN" { + return attribute.Value, nil + } + } + } + return "", errors.New("failed to find dn") +} + +func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { + jar, err := cookiejar.New(nil) + if err != nil { + pi.log.WithError(err).Warning("Failed to create cookiejar") + return ldap.LDAPResultOperationsError, nil + } + client := &http.Client{ + Jar: jar, + } + passed, err := pi.solveFlowChallenge(username, bindPW, client) + if err != nil { + pi.log.WithField("boundDN", username).WithError(err).Warning("failed to solve challenge") + return ldap.LDAPResultOperationsError, nil + } + if !passed { + return ldap.LDAPResultInvalidCredentials, nil + } + _, err = pi.s.ac.Client.Core.CoreApplicationsCheckAccess(&core.CoreApplicationsCheckAccessParams{ + Slug: pi.appSlug, + Context: context.Background(), + HTTPClient: client, + }, httptransport.PassThroughAuth) + if err != nil { + if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied { + pi.log.WithField("boundDN", username).Info("Access denied for user") + return ldap.LDAPResultInsufficientAccessRights, nil + } + pi.log.WithField("boundDN", username).WithError(err).Warning("failed to check access") + return ldap.LDAPResultOperationsError, nil + } + pi.log.WithField("boundDN", username).Info("User has access") + // Get user info to store in context + userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{ + Context: context.Background(), + HTTPClient: client, + }, httptransport.PassThroughAuth) + if err != nil { + pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info") + return ldap.LDAPResultOperationsError, nil + } + pi.boundUsersMutex.Lock() + pi.boundUsers[username] = UserFlags{ + UserInfo: userInfo.Payload.User, + CanSearch: pi.SearchAccessCheck(userInfo.Payload.User), + } + pi.boundUsersMutex.Unlock() + pi.delayDeleteUserInfo(username) + return ldap.LDAPResultSuccess, nil +} + +// SearchAccessCheck Check if the current user is allowed to search +func (pi *ProviderInstance) SearchAccessCheck(user *models.User) bool { + for _, group := range user.Groups { + for _, allowedGroup := range pi.searchAllowedGroups { + if &group.Pk == allowedGroup { + pi.log.WithField("group", group.Name).Info("Allowed access to search") + return true + } + } + } + return false +} +func (pi *ProviderInstance) delayDeleteUserInfo(dn string) { + ticker := time.NewTicker(30 * time.Second) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + pi.boundUsersMutex.Lock() + delete(pi.boundUsers, dn) + pi.boundUsersMutex.Unlock() + close(quit) + case <-quit: + ticker.Stop() + return + } + } + }() +} + +func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) { + challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ + FlowSlug: pi.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + }, httptransport.PassThroughAuth) + if err != nil { + pi.log.WithError(err).Warning("Failed to get challenge") + return false, err + } + pi.log.WithField("component", challenge.Payload.Component).WithField("type", *challenge.Payload.Type).Debug("Got challenge") + responseParams := &flows.FlowsExecutorSolveParams{ + FlowSlug: pi.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + } + switch challenge.Payload.Component { + case "ak-stage-identification": + responseParams.Data = &UIDResponse{UIDFIeld: bindDN} + case "ak-stage-password": + responseParams.Data = &PasswordResponse{Password: password} + default: + return false, fmt.Errorf("unsupported challenge type: %s", challenge.Payload.Component) + } + response, err := pi.s.ac.Client.Flows.FlowsExecutorSolve(responseParams, httptransport.PassThroughAuth) + pi.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") + if *response.Payload.Type == "redirect" { + return true, nil + } + if err != nil { + pi.log.WithError(err).Warning("Failed to submit challenge") + return false, err + } + if len(response.Payload.ResponseErrors) > 0 { + for key, errs := range response.Payload.ResponseErrors { + for _, err := range errs { + pi.log.WithField("key", key).WithField("code", *err.Code).Debug(*err.String) + return false, nil + } + } + } + return pi.solveFlowChallenge(bindDN, password, client) +} diff --git a/outpost/pkg/ldap/instance_search.go b/outpost/pkg/ldap/instance_search.go new file mode 100644 index 000000000..15bc352f7 --- /dev/null +++ b/outpost/pkg/ldap/instance_search.go @@ -0,0 +1,124 @@ +package ldap + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" + + "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/client/core" +) + +func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { + bindDN = strings.ToLower(bindDN) + baseDN := strings.ToLower("," + pi.BaseDN) + + entries := []*ldap.Entry{} + filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter) + } + if len(bindDN) < 1 { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) + } + if !strings.HasSuffix(bindDN, baseDN) { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, pi.BaseDN) + } + + pi.boundUsersMutex.RLock() + defer pi.boundUsersMutex.RUnlock() + flags, ok := pi.boundUsers[bindDN] + if !ok { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("Access denied") + } + if !flags.CanSearch { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("Access denied") + } + + switch filterEntity { + default: + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) + case GroupObjectClass: + groups, err := pi.s.ac.Client.Core.CoreGroupsList(core.NewCoreGroupsListParams(), pi.s.ac.Auth) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) + } + pi.log.WithField("count", len(groups.Payload.Results)).Trace("Got results from API") + for _, g := range groups.Payload.Results { + attrs := []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{*g.Name}, + }, + { + Name: "uid", + Values: []string{string(g.Pk)}, + }, + { + Name: "objectClass", + Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, + }, + } + attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...) + + dn := pi.GetGroupDN(g) + entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) + } + case UserObjectClass, "": + users, err := pi.s.ac.Client.Core.CoreUsersList(core.NewCoreUsersListParams(), pi.s.ac.Auth) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) + } + for _, u := range users.Payload.Results { + attrs := []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{*u.Username}, + }, + { + Name: "uid", + Values: []string{strconv.Itoa(int(u.Pk))}, + }, + { + Name: "name", + Values: []string{*u.Name}, + }, + { + Name: "displayName", + Values: []string{*u.Name}, + }, + { + Name: "mail", + Values: []string{u.Email.String()}, + }, + { + Name: "objectClass", + Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"}, + }, + } + + if u.IsActive { + attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}}) + } else { + attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}}) + } + + if *u.IsSuperuser { + attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}}) + } else { + attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}}) + } + + attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)}) + + attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...) + + dn := fmt.Sprintf("cn=%s,%s", *u.Name, pi.UserDN) + entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) + } + } + pi.log.WithField("filter", searchReq.Filter).Debug("Search OK") + return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil +} diff --git a/outpost/pkg/ldap/ldap.go b/outpost/pkg/ldap/ldap.go new file mode 100644 index 000000000..a22348639 --- /dev/null +++ b/outpost/pkg/ldap/ldap.go @@ -0,0 +1,58 @@ +package ldap + +import ( + "sync" + + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" + "goauthentik.io/outpost/pkg/ak" + "goauthentik.io/outpost/pkg/models" + + "github.com/nmcclain/ldap" +) + +const GroupObjectClass = "group" +const UserObjectClass = "user" + +type ProviderInstance struct { + BaseDN string + + UserDN string + GroupDN string + + appSlug string + flowSlug string + s *LDAPServer + log *log.Entry + + searchAllowedGroups []*strfmt.UUID + boundUsersMutex sync.RWMutex + boundUsers map[string]UserFlags +} + +type UserFlags struct { + UserInfo *models.User + CanSearch bool +} + +type LDAPServer struct { + s *ldap.Server + log *log.Entry + ac *ak.APIController + + providers []*ProviderInstance +} + +func NewServer(ac *ak.APIController) *LDAPServer { + s := ldap.NewServer() + s.EnforceLDAP = true + ls := &LDAPServer{ + s: s, + log: log.WithField("logger", "authentik.outpost.ldap"), + ac: ac, + providers: []*ProviderInstance{}, + } + s.BindFunc("", ls) + s.SearchFunc("", ls) + return ls +} diff --git a/outpost/pkg/ldap/search.go b/outpost/pkg/ldap/search.go new file mode 100644 index 000000000..ecc5f35e6 --- /dev/null +++ b/outpost/pkg/ldap/search.go @@ -0,0 +1,28 @@ +package ldap + +import ( + "errors" + "net" + + goldap "github.com/go-ldap/ldap/v3" + "github.com/nmcclain/ldap" +) + +func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { + ls.log.WithField("boundDN", boundDN).WithField("baseDN", searchReq.BaseDN).Info("search") + if searchReq.BaseDN == "" { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil + } + bd, err := goldap.ParseDN(searchReq.BaseDN) + if err != nil { + ls.log.WithField("baseDN", searchReq.BaseDN).WithError(err).Info("failed to parse basedn") + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN") + } + for _, provider := range ls.providers { + providerBase, _ := goldap.ParseDN(provider.BaseDN) + if providerBase.AncestorOf(bd) { + return provider.Search(boundDN, searchReq, conn) + } + } + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request") +} diff --git a/outpost/pkg/ldap/utils.go b/outpost/pkg/ldap/utils.go new file mode 100644 index 000000000..244c199f8 --- /dev/null +++ b/outpost/pkg/ldap/utils.go @@ -0,0 +1,35 @@ +package ldap + +import ( + "fmt" + + "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/models" +) + +func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute { + attrList := []*ldap.EntryAttribute{} + for attrKey, attrValue := range attrs.(map[string]interface{}) { + entry := &ldap.EntryAttribute{Name: attrKey} + switch t := attrValue.(type) { + case []string: + entry.Values = t + case string: + entry.Values = []string{t} + } + attrList = append(attrList, entry) + } + return attrList +} + +func (pi *ProviderInstance) GroupsForUser(user *models.User) []string { + groups := make([]string, len(user.Groups)) + for i, group := range user.Groups { + groups[i] = pi.GetGroupDN(group) + } + return groups +} + +func (pi *ProviderInstance) GetGroupDN(group *models.Group) string { + return fmt.Sprintf("cn=%s,%s", *group.Name, pi.GroupDN) +} diff --git a/outpost/proxy.Dockerfile b/outpost/proxy.Dockerfile index 99e81d256..4da2cafcc 100644 --- a/outpost/proxy.Dockerfile +++ b/outpost/proxy.Dockerfile @@ -6,7 +6,6 @@ COPY . . RUN go build -o /work/proxy ./cmd/proxy -# Copy binary to alpine FROM gcr.io/distroless/base-debian10:debug COPY --from=builder /work/proxy / diff --git a/swagger.yaml b/swagger.yaml index 9c755dec6..039f99cbf 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1329,6 +1329,33 @@ paths: type: string format: slug pattern: ^[-a-zA-Z0-9_]+$ + /core/applications/{slug}/check_access/: + get: + operationId: core_applications_check_access + description: Check access to a single application by slug + parameters: [] + responses: + '204': + description: Access granted + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - core + parameters: + - name: slug + in: path + description: Internal application name, used in URLs. + required: true + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ /core/applications/{slug}/metrics/: get: operationId: core_applications_metrics @@ -4649,6 +4676,103 @@ paths: required: true type: string format: uuid + /outposts/ldap/: + get: + operationId: outposts_ldap_list + description: LDAPProvider Viewset + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + 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 + results: + type: array + items: + $ref: '#/definitions/LDAPOutpostConfig' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + tags: + - outposts + parameters: [] + /outposts/ldap/{id}/: + get: + operationId: outposts_ldap_read + description: LDAPProvider Viewset + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/LDAPOutpostConfig' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - outposts + parameters: + - name: id + in: path + description: A unique integer value identifying this LDAP Provider. + required: true + type: integer /outposts/outposts/: get: operationId: outposts_outposts_list @@ -8717,6 +8841,203 @@ paths: description: A unique integer value identifying this provider. required: true type: integer + /providers/ldap/: + get: + operationId: providers_ldap_list + description: LDAPProvider Viewset + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + 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 + results: + type: array + items: + $ref: '#/definitions/LDAPProvider' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + tags: + - providers + post: + operationId: providers_ldap_create + description: LDAPProvider Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/LDAPProvider' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/LDAPProvider' + '400': + description: Invalid input. + schema: + $ref: '#/definitions/ValidationError' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + tags: + - providers + parameters: [] + /providers/ldap/{id}/: + get: + operationId: providers_ldap_read + description: LDAPProvider Viewset + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/LDAPProvider' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - providers + put: + operationId: providers_ldap_update + description: LDAPProvider Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/LDAPProvider' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/LDAPProvider' + '400': + description: Invalid input. + schema: + $ref: '#/definitions/ValidationError' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - providers + patch: + operationId: providers_ldap_partial_update + description: LDAPProvider Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/LDAPProvider' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/LDAPProvider' + '400': + description: Invalid input. + schema: + $ref: '#/definitions/ValidationError' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - providers + delete: + operationId: providers_ldap_delete + description: LDAPProvider Viewset + parameters: [] + responses: + '204': + description: '' + '403': + description: Authentication credentials were invalid, absent or insufficient. + schema: + $ref: '#/definitions/GenericError' + '404': + description: Object does not exist or caller has insufficient permissions + to access it. + schema: + $ref: '#/definitions/APIException' + tags: + - providers + parameters: + - name: id + in: path + description: A unique integer value identifying this LDAP Provider. + required: true + type: integer /providers/oauth2/: get: operationId: providers_oauth2_list @@ -15056,6 +15377,11 @@ definitions: title: Is superuser type: boolean readOnly: true + groups: + type: array + items: + $ref: '#/definitions/Group' + readOnly: true email: title: Email address type: string @@ -15881,6 +16207,12 @@ definitions: title: Name type: string minLength: 1 + type: + title: Type + type: string + enum: + - proxy + - ldap providers: type: array items: @@ -15934,6 +16266,41 @@ definitions: title: Version outdated type: boolean readOnly: true + LDAPOutpostConfig: + required: + - name + - bind_flow_slug + - application_slug + type: object + properties: + pk: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + minLength: 1 + base_dn: + title: Base dn + description: DN under which objects are accessible. + type: string + minLength: 1 + bind_flow_slug: + title: Bind flow slug + type: string + minLength: 1 + application_slug: + title: Application slug + type: string + minLength: 1 + search_group: + title: Search group + description: Users in this group can do search queries. If not set, every + user can execute search queries. + type: string + format: uuid + x-nullable: true OpenIDConnectConfiguration: description: Embed OpenID Connect provider information required: @@ -16442,6 +16809,7 @@ definitions: - authentik.policies.password - authentik.policies.reputation - authentik.providers.proxy + - authentik.providers.ldap - authentik.providers.oauth2 - authentik.providers.saml - authentik.recovery @@ -16947,6 +17315,63 @@ definitions: description: Description shown to the user when consenting. If left empty, the user won't be informed. type: string + LDAPProvider: + required: + - name + - authorization_flow + type: object + properties: + pk: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + minLength: 1 + authorization_flow: + title: Authorization flow + description: Flow used when authorizing this provider. + type: string + format: uuid + property_mappings: + type: array + items: + type: string + format: uuid + uniqueItems: true + component: + title: Component + type: string + readOnly: true + assigned_application_slug: + title: Assigned application slug + type: string + readOnly: true + assigned_application_name: + title: Assigned application name + type: string + readOnly: true + verbose_name: + title: Verbose name + type: string + readOnly: true + verbose_name_plural: + title: Verbose name plural + type: string + readOnly: true + base_dn: + title: Base dn + description: DN under which objects are accessible. + type: string + minLength: 1 + search_group: + title: Search group + description: Users in this group can do search queries. If not set, every + user can execute search queries. + type: string + format: uuid + x-nullable: true OAuth2ProviderSetupURLs: type: object properties: diff --git a/web/azure-pipelines.yml b/web/azure-pipelines.yml index 4123cc83e..dca4a3d4f 100644 --- a/web/azure-pipelines.yml +++ b/web/azure-pipelines.yml @@ -12,7 +12,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: '14.x' + versionSpec: '16.x' displayName: 'Install Node.js' - task: CmdLine@2 inputs: @@ -31,7 +31,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: '14.x' + versionSpec: '16.x' displayName: 'Install Node.js' - task: DownloadPipelineArtifact@2 inputs: @@ -53,7 +53,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: '14.x' + versionSpec: '16.x' displayName: 'Install Node.js' - task: DownloadPipelineArtifact@2 inputs: @@ -77,7 +77,7 @@ stages: steps: - task: NodeTool@0 inputs: - versionSpec: '14.x' + versionSpec: '16.x' displayName: 'Install Node.js' - task: DownloadPipelineArtifact@2 inputs: diff --git a/web/package.json b/web/package.json index aa02c95aa..15ab40387 100644 --- a/web/package.json +++ b/web/package.json @@ -87,5 +87,6 @@ "typescript": "^4.2.4", "webcomponent-qr-code": "^1.0.5", "yaml": "^1.10.2" - } + }, + "devDependencies": {} } diff --git a/web/src/flows/sources/plex/API.ts b/web/src/flows/sources/plex/API.ts index be7a46d59..7f0fd3731 100644 --- a/web/src/flows/sources/plex/API.ts +++ b/web/src/flows/sources/plex/API.ts @@ -11,6 +11,7 @@ export interface PlexResource { name: string; provides: string; clientIdentifier: string; + owned: boolean; } export const DEFAULT_HEADERS = { @@ -88,7 +89,7 @@ export class PlexAPIClient { }); const resources: PlexResource[] = await resourcesResponse.json(); return resources.filter(r => { - return r.provides === "server"; + return r.provides.toLowerCase().includes("server") && r.owned; }); } diff --git a/web/src/locales/en.po b/web/src/locales/en.po index df1e16fb1..2a5e043fa 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -91,7 +91,7 @@ msgid "Action" msgstr "Action" #: src/pages/groups/MemberSelectModal.ts:46 -#: src/pages/users/UserListPage.ts:51 +#: src/pages/users/UserListPage.ts:55 #: src/pages/users/UserViewPage.ts:116 msgid "Active" msgstr "Active" @@ -767,8 +767,8 @@ msgstr "Copy Key" #: src/pages/stages/prompt/PromptStageForm.ts:98 #: src/pages/user-settings/tokens/UserTokenList.ts:50 #: src/pages/user-settings/tokens/UserTokenList.ts:58 -#: src/pages/users/UserListPage.ts:151 -#: src/pages/users/UserListPage.ts:159 +#: src/pages/users/UserListPage.ts:155 +#: src/pages/users/UserListPage.ts:163 msgid "Create" msgstr "Create" @@ -838,7 +838,7 @@ msgstr "Create Stage binding" msgid "Create Token" msgstr "Create Token" -#: src/pages/users/UserListPage.ts:154 +#: src/pages/users/UserListPage.ts:158 msgid "Create User" msgstr "Create User" @@ -916,7 +916,7 @@ msgstr "Define how notifications are sent to users, like Email or Webhook." #: src/pages/tokens/TokenListPage.ts:68 #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40 #: src/pages/user-settings/tokens/UserTokenList.ts:125 -#: src/pages/users/UserListPage.ts:115 +#: src/pages/users/UserListPage.ts:119 msgid "Delete" msgstr "Delete" @@ -1006,8 +1006,8 @@ msgstr "Digest algorithm" msgid "Digits" msgstr "Digits" -#: src/pages/users/UserListPage.ts:81 -#: src/pages/users/UserListPage.ts:100 +#: src/pages/users/UserListPage.ts:85 +#: src/pages/users/UserListPage.ts:104 msgid "Disable" msgstr "Disable" @@ -1068,7 +1068,7 @@ msgstr "Each provider has a different issuer, based on the application slug." #: src/pages/stages/StageListPage.ts:98 #: src/pages/stages/prompt/PromptListPage.ts:75 #: src/pages/user-settings/tokens/UserTokenList.ts:113 -#: src/pages/users/UserListPage.ts:76 +#: src/pages/users/UserListPage.ts:80 #: src/pages/users/UserViewPage.ts:147 msgid "Edit" msgstr "Edit" @@ -1119,8 +1119,8 @@ msgstr "Email or username" msgid "Email: Text field with Email type." msgstr "Email: Text field with Email type." -#: src/pages/users/UserListPage.ts:81 -#: src/pages/users/UserListPage.ts:100 +#: src/pages/users/UserListPage.ts:85 +#: src/pages/users/UserListPage.ts:104 msgid "Enable" msgstr "Enable" @@ -1520,6 +1520,10 @@ msgstr "Hidden: Hidden field, can be used to insert data into form." msgid "Hide managed mappings" msgstr "Hide managed mappings" +#: src/pages/users/UserListPage.ts:186 +msgid "Hide service-accounts" +msgstr "Hide service-accounts" + #: src/pages/events/RuleForm.ts:93 #: src/pages/groups/GroupForm.ts:131 #: src/pages/outposts/OutpostForm.ts:98 @@ -1577,7 +1581,7 @@ msgstr "If this flag is set, this Stage will jump to the next Stage when no Invi msgid "If your authentik Instance is using a self-signed certificate, set this value." msgstr "If your authentik Instance is using a self-signed certificate, set this value." -#: src/pages/users/UserListPage.ts:143 +#: src/pages/users/UserListPage.ts:147 msgid "Impersonate" msgstr "Impersonate" @@ -1680,7 +1684,7 @@ msgid "Label shown next to/above the prompt." msgstr "Label shown next to/above the prompt." #: src/pages/groups/MemberSelectModal.ts:47 -#: src/pages/users/UserListPage.ts:52 +#: src/pages/users/UserListPage.ts:56 #: src/pages/users/UserViewPage.ts:108 msgid "Last login" msgstr "Last login" @@ -1986,7 +1990,7 @@ msgstr "Monitor" #: src/pages/stages/user_write/UserWriteStageForm.ts:55 #: src/pages/user-settings/UserDetailsPage.ts:64 #: src/pages/users/UserForm.ts:54 -#: src/pages/users/UserListPage.ts:50 +#: src/pages/users/UserListPage.ts:54 #: src/pages/users/UserViewPage.ts:92 msgid "Name" msgstr "Name" @@ -2020,7 +2024,7 @@ msgstr "New version available!" #: src/pages/providers/proxy/ProxyProviderViewPage.ts:108 #: src/pages/tokens/TokenListPage.ts:56 #: src/pages/user-settings/tokens/UserTokenList.ts:83 -#: src/pages/users/UserListPage.ts:63 +#: src/pages/users/UserListPage.ts:67 msgid "No" msgstr "No" @@ -2070,7 +2074,7 @@ msgstr "No policies are currently bound to this object." msgid "No policies cached. Users may experience slow response times." msgstr "No policies cached. Users may experience slow response times." -#: src/pages/users/UserListPage.ts:135 +#: src/pages/users/UserListPage.ts:139 msgid "No recovery flow is configured." msgstr "No recovery flow is configured." @@ -2628,7 +2632,7 @@ msgstr "Required" msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." -#: src/pages/users/UserListPage.ts:140 +#: src/pages/users/UserListPage.ts:144 #: src/pages/users/UserViewPage.ts:165 msgid "Reset Password" msgstr "Reset Password" @@ -3172,7 +3176,7 @@ msgstr "Successfully deleted {0} {1}" msgid "Successfully generated certificate-key pair." msgstr "Successfully generated certificate-key pair." -#: src/pages/users/UserListPage.ts:128 +#: src/pages/users/UserListPage.ts:132 #: src/pages/users/UserViewPage.ts:160 msgid "Successfully generated recovery link" msgstr "Successfully generated recovery link" @@ -3613,7 +3617,7 @@ msgstr "Up-to-date!" #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:71 #: src/pages/user-settings/tokens/UserTokenList.ts:105 #: src/pages/users/UserActiveForm.ts:66 -#: src/pages/users/UserListPage.ts:68 +#: src/pages/users/UserListPage.ts:72 #: src/pages/users/UserViewPage.ts:139 msgid "Update" msgstr "Update" @@ -3693,7 +3697,7 @@ msgid "Update Token" msgstr "Update Token" #: src/pages/policies/BoundPoliciesList.ts:106 -#: src/pages/users/UserListPage.ts:71 +#: src/pages/users/UserListPage.ts:75 #: src/pages/users/UserViewPage.ts:142 msgid "Update User" msgstr "Update User" @@ -3758,8 +3762,8 @@ msgstr "Use the user's username, but deny enrollment when the username already e #: src/pages/property-mappings/PropertyMappingTestForm.ts:51 #: src/pages/tokens/TokenListPage.ts:45 #: src/pages/user-settings/tokens/UserTokenList.ts:72 -#: src/pages/users/UserListPage.ts:88 -#: src/pages/users/UserListPage.ts:108 +#: src/pages/users/UserListPage.ts:92 +#: src/pages/users/UserListPage.ts:112 msgid "User" msgstr "User" @@ -3841,7 +3845,7 @@ msgstr "Username: Same as Text input, but checks for and prevents duplicate user #: src/interfaces/AdminInterface.ts:32 #: src/pages/admin-overview/AdminOverviewPage.ts:50 -#: src/pages/users/UserListPage.ts:32 +#: src/pages/users/UserListPage.ts:33 msgid "Users" msgstr "Users" @@ -4013,7 +4017,7 @@ msgstr "X509 Subject" #: src/pages/providers/proxy/ProxyProviderViewPage.ts:105 #: src/pages/tokens/TokenListPage.ts:56 #: src/pages/user-settings/tokens/UserTokenList.ts:83 -#: src/pages/users/UserListPage.ts:63 +#: src/pages/users/UserListPage.ts:67 msgid "Yes" msgstr "Yes" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index e87c4f6ca..0d7656c60 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -91,7 +91,7 @@ msgid "Action" msgstr "" #: src/pages/groups/MemberSelectModal.ts:46 -#: src/pages/users/UserListPage.ts:51 +#: src/pages/users/UserListPage.ts:55 #: src/pages/users/UserViewPage.ts:116 msgid "Active" msgstr "" @@ -761,8 +761,8 @@ msgstr "" #: src/pages/stages/prompt/PromptStageForm.ts:98 #: src/pages/user-settings/tokens/UserTokenList.ts:50 #: src/pages/user-settings/tokens/UserTokenList.ts:58 -#: src/pages/users/UserListPage.ts:151 -#: src/pages/users/UserListPage.ts:159 +#: src/pages/users/UserListPage.ts:155 +#: src/pages/users/UserListPage.ts:163 msgid "Create" msgstr "" @@ -832,7 +832,7 @@ msgstr "" msgid "Create Token" msgstr "" -#: src/pages/users/UserListPage.ts:154 +#: src/pages/users/UserListPage.ts:158 msgid "Create User" msgstr "" @@ -910,7 +910,7 @@ msgstr "" #: src/pages/tokens/TokenListPage.ts:68 #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40 #: src/pages/user-settings/tokens/UserTokenList.ts:125 -#: src/pages/users/UserListPage.ts:115 +#: src/pages/users/UserListPage.ts:119 msgid "Delete" msgstr "" @@ -998,8 +998,8 @@ msgstr "" msgid "Digits" msgstr "" -#: src/pages/users/UserListPage.ts:81 -#: src/pages/users/UserListPage.ts:100 +#: src/pages/users/UserListPage.ts:85 +#: src/pages/users/UserListPage.ts:104 msgid "Disable" msgstr "" @@ -1060,7 +1060,7 @@ msgstr "" #: src/pages/stages/StageListPage.ts:98 #: src/pages/stages/prompt/PromptListPage.ts:75 #: src/pages/user-settings/tokens/UserTokenList.ts:113 -#: src/pages/users/UserListPage.ts:76 +#: src/pages/users/UserListPage.ts:80 #: src/pages/users/UserViewPage.ts:147 msgid "Edit" msgstr "" @@ -1111,8 +1111,8 @@ msgstr "" msgid "Email: Text field with Email type." msgstr "" -#: src/pages/users/UserListPage.ts:81 -#: src/pages/users/UserListPage.ts:100 +#: src/pages/users/UserListPage.ts:85 +#: src/pages/users/UserListPage.ts:104 msgid "Enable" msgstr "" @@ -1512,6 +1512,10 @@ msgstr "" msgid "Hide managed mappings" msgstr "" +#: src/pages/users/UserListPage.ts:186 +msgid "Hide service-accounts" +msgstr "" + #: src/pages/events/RuleForm.ts:93 #: src/pages/groups/GroupForm.ts:131 #: src/pages/outposts/OutpostForm.ts:98 @@ -1569,7 +1573,7 @@ msgstr "" msgid "If your authentik Instance is using a self-signed certificate, set this value." msgstr "" -#: src/pages/users/UserListPage.ts:143 +#: src/pages/users/UserListPage.ts:147 msgid "Impersonate" msgstr "" @@ -1672,7 +1676,7 @@ msgid "Label shown next to/above the prompt." msgstr "" #: src/pages/groups/MemberSelectModal.ts:47 -#: src/pages/users/UserListPage.ts:52 +#: src/pages/users/UserListPage.ts:56 #: src/pages/users/UserViewPage.ts:108 msgid "Last login" msgstr "" @@ -1978,7 +1982,7 @@ msgstr "" #: src/pages/stages/user_write/UserWriteStageForm.ts:55 #: src/pages/user-settings/UserDetailsPage.ts:64 #: src/pages/users/UserForm.ts:54 -#: src/pages/users/UserListPage.ts:50 +#: src/pages/users/UserListPage.ts:54 #: src/pages/users/UserViewPage.ts:92 msgid "Name" msgstr "" @@ -2012,7 +2016,7 @@ msgstr "" #: src/pages/providers/proxy/ProxyProviderViewPage.ts:108 #: src/pages/tokens/TokenListPage.ts:56 #: src/pages/user-settings/tokens/UserTokenList.ts:83 -#: src/pages/users/UserListPage.ts:63 +#: src/pages/users/UserListPage.ts:67 msgid "No" msgstr "" @@ -2062,7 +2066,7 @@ msgstr "" msgid "No policies cached. Users may experience slow response times." msgstr "" -#: src/pages/users/UserListPage.ts:135 +#: src/pages/users/UserListPage.ts:139 msgid "No recovery flow is configured." msgstr "" @@ -2620,7 +2624,7 @@ msgstr "" msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgstr "" -#: src/pages/users/UserListPage.ts:140 +#: src/pages/users/UserListPage.ts:144 #: src/pages/users/UserViewPage.ts:165 msgid "Reset Password" msgstr "" @@ -3164,7 +3168,7 @@ msgstr "" msgid "Successfully generated certificate-key pair." msgstr "" -#: src/pages/users/UserListPage.ts:128 +#: src/pages/users/UserListPage.ts:132 #: src/pages/users/UserViewPage.ts:160 msgid "Successfully generated recovery link" msgstr "" @@ -3601,7 +3605,7 @@ msgstr "" #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:71 #: src/pages/user-settings/tokens/UserTokenList.ts:105 #: src/pages/users/UserActiveForm.ts:66 -#: src/pages/users/UserListPage.ts:68 +#: src/pages/users/UserListPage.ts:72 #: src/pages/users/UserViewPage.ts:139 msgid "Update" msgstr "" @@ -3681,7 +3685,7 @@ msgid "Update Token" msgstr "" #: src/pages/policies/BoundPoliciesList.ts:106 -#: src/pages/users/UserListPage.ts:71 +#: src/pages/users/UserListPage.ts:75 #: src/pages/users/UserViewPage.ts:142 msgid "Update User" msgstr "" @@ -3746,8 +3750,8 @@ msgstr "" #: src/pages/property-mappings/PropertyMappingTestForm.ts:51 #: src/pages/tokens/TokenListPage.ts:45 #: src/pages/user-settings/tokens/UserTokenList.ts:72 -#: src/pages/users/UserListPage.ts:88 -#: src/pages/users/UserListPage.ts:108 +#: src/pages/users/UserListPage.ts:92 +#: src/pages/users/UserListPage.ts:112 msgid "User" msgstr "" @@ -3829,7 +3833,7 @@ msgstr "" #: src/interfaces/AdminInterface.ts:32 #: src/pages/admin-overview/AdminOverviewPage.ts:50 -#: src/pages/users/UserListPage.ts:32 +#: src/pages/users/UserListPage.ts:33 msgid "Users" msgstr "" @@ -3999,7 +4003,7 @@ msgstr "" #: src/pages/providers/proxy/ProxyProviderViewPage.ts:105 #: src/pages/tokens/TokenListPage.ts:56 #: src/pages/user-settings/tokens/UserTokenList.ts:83 -#: src/pages/users/UserListPage.ts:63 +#: src/pages/users/UserListPage.ts:67 msgid "Yes" msgstr "" diff --git a/web/src/pages/outposts/OutpostForm.ts b/web/src/pages/outposts/OutpostForm.ts index af3d3cbd2..dfec1651e 100644 --- a/web/src/pages/outposts/OutpostForm.ts +++ b/web/src/pages/outposts/OutpostForm.ts @@ -1,4 +1,4 @@ -import { Outpost, OutpostsApi, ProvidersApi } from "authentik-api"; +import { Outpost, OutpostsApi, OutpostTypeEnum, ProvidersApi } from "authentik-api"; import { t } from "@lingui/macro"; import { customElement, property } from "lit-element"; import { html, TemplateResult } from "lit-html"; @@ -50,7 +50,8 @@ export class OutpostForm extends Form { ?required=${true} name="type"> { return html``; }); }), html``)} + ${until(new ProvidersApi(DEFAULT_CONFIG).providersLdapList({ + ordering: "pk" + }).then(providers => { + return providers.results.map(provider => { + const selected = Array.from(this.outpost?.providers || []).some(sp => { + return sp == provider.pk; + }); + return html``; + }); + }), html``)}

${t`Hold control/command to select multiple items.`}

diff --git a/web/src/pages/providers/ProviderListPage.ts b/web/src/pages/providers/ProviderListPage.ts index 000cfe211..c92011e21 100644 --- a/web/src/pages/providers/ProviderListPage.ts +++ b/web/src/pages/providers/ProviderListPage.ts @@ -8,6 +8,7 @@ import "../../elements/buttons/Dropdown"; import "../../elements/forms/DeleteForm"; import "../../elements/forms/ModalForm"; import "../../elements/forms/ProxyForm"; +import "./ldap/LDAPProviderForm"; import "./oauth2/OAuth2ProviderForm"; import "./proxy/ProxyProviderForm"; import "./saml/SAMLProviderForm"; diff --git a/web/src/pages/providers/ldap/LDAPProviderForm.ts b/web/src/pages/providers/ldap/LDAPProviderForm.ts new file mode 100644 index 000000000..4a21f4766 --- /dev/null +++ b/web/src/pages/providers/ldap/LDAPProviderForm.ts @@ -0,0 +1,103 @@ +import { FlowDesignationEnum, FlowsApi, ProvidersApi, LDAPProvider, CoreApi } from "authentik-api"; +import { t } from "@lingui/macro"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { Form } from "../../../elements/forms/Form"; +import { until } from "lit-html/directives/until"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../../elements/forms/HorizontalFormElement"; +import "../../../elements/forms/FormGroup"; +import { first } from "../../../utils"; + +@customElement("ak-provider-ldap-form") +export class LDAPProviderFormPage extends Form { + + set providerUUID(value: number) { + new ProvidersApi(DEFAULT_CONFIG).providersLdapRead({ + id: value, + }).then(provider => { + this.provider = provider; + }); + } + + @property({attribute: false}) + provider?: LDAPProvider; + + getSuccessMessage(): string { + if (this.provider) { + return t`Successfully updated provider.`; + } else { + return t`Successfully created provider.`; + } + } + + send = (data: LDAPProvider): Promise => { + if (this.provider) { + return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({ + id: this.provider.pk || 0, + data: data + }); + } else { + return new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + + +

${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}

+
+ + +

${t`Users in the selected group can do search queries.`}

+
+ + + + ${t`Protocol settings`} + +
+ + +

${t`LDAP DN under which bind requests and search requests can be made.`}

+
+
+
+
`; + } + +} diff --git a/web/src/pages/providers/ldap/LDAPProviderViewPage.ts b/web/src/pages/providers/ldap/LDAPProviderViewPage.ts new file mode 100644 index 000000000..e8bc97667 --- /dev/null +++ b/web/src/pages/providers/ldap/LDAPProviderViewPage.ts @@ -0,0 +1,129 @@ +import { t } from "@lingui/macro"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; +import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css"; +import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css"; +import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import AKGlobal from "../../../authentik.css"; + +import "../../../elements/buttons/ModalButton"; +import "../../../elements/buttons/SpinnerButton"; +import "../../../elements/CodeMirror"; +import "../../../elements/Tabs"; +import "../../../elements/events/ObjectChangelog"; +import "../RelatedApplicationButton"; +import "./LDAPProviderForm"; +import { ProvidersApi, LDAPProvider } from "authentik-api"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { EVENT_REFRESH } from "../../../constants"; + +@customElement("ak-provider-ldap-view") +export class LDAPProviderViewPage extends LitElement { + + @property() + set args(value: { [key: string]: number }) { + this.providerID = value.id; + } + + @property({type: Number}) + set providerID(value: number) { + new ProvidersApi(DEFAULT_CONFIG).providersLdapRead({ + id: value, + }).then((prov) => (this.provider = prov)); + } + + @property({ attribute: false }) + provider?: LDAPProvider; + + static get styles(): CSSResult[] { + return [PFBase, PFButton, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal]; + } + + constructor() { + super(); + this.addEventListener(EVENT_REFRESH, () => { + if (!this.provider?.pk) return; + this.providerID = this.provider?.pk; + }); + } + + render(): TemplateResult { + if (!this.provider) { + return html``; + } + return html` +
+
+
+
+
+
+
+
+ ${t`Name`} +
+
+
${this.provider.name}
+
+
+
+
+ ${t`Assigned to application`} +
+
+
+ +
+
+
+
+
+ ${t`Base DN`} +
+
+
${this.provider.baseDn}
+
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
`; + } +} diff --git a/web/src/pages/sources/oauth/OAuthSourceForm.ts b/web/src/pages/sources/oauth/OAuthSourceForm.ts index 8a82e378c..d997ce690 100644 --- a/web/src/pages/sources/oauth/OAuthSourceForm.ts +++ b/web/src/pages/sources/oauth/OAuthSourceForm.ts @@ -156,6 +156,7 @@ export class OAuthSourceForm extends Form {