Outpost LDAP (#784)
* outposts: initial ldap outpost implementation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: add LDAP Binding using flows Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: add API to check access to single application by slug Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: check application access Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/ldap: add LDAP provider Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: add ability to use multiple providers on the same outpost Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add UI for LDAP Provider Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: fix linting Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: add controllers Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: fix type not being configurable Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: use authorization_flow instead of separate field Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: add dockerfile Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/ldap: fix lint error Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: add groups to users Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/ldap: add search_group to limit who can do search requests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: improve logging,return success for empty DN Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: allow outposts to have non-object specific permissions Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: use forked version of ldap library Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/ldap: save user DN to determine who can search Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * */api: fix lookups per user Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: only show plex servers you own Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * lib: add support for file:// protocol in config file Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: hide oauth client secret if not updating Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost/ldap: check access based on Group Membership Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: show users and groups when user has overall user permissions Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * lib: handle errors when reading config from file:// Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix package json failing Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ci: bump node spec to 16x for npm version and lockfile v2 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
commit
ca89201bd8
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -24,6 +24,7 @@ class OutpostSerializer(ModelSerializer):
|
|||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"type",
|
||||
"providers",
|
||||
"providers_obj",
|
||||
"service_connection",
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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()
|
||||
):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
|
@ -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"
|
|
@ -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"),
|
||||
]
|
|
@ -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"),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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"]
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 /
|
||||
|
|
425
swagger.yaml
425
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -87,5 +87,6 @@
|
|||
"typescript": "^4.2.4",
|
||||
"webcomponent-qr-code": "^1.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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<Outpost> {
|
|||
?required=${true}
|
||||
name="type">
|
||||
<select class="pf-c-form-control">
|
||||
<option value="proxy" ?selected=${true}>${t`Proxy`}</option>s
|
||||
<option value=${OutpostTypeEnum.Proxy} ?selected=${this.outpost?.type === OutpostTypeEnum.Proxy}>${t`Proxy`}</option>
|
||||
<option value=${OutpostTypeEnum.Ldap} ?selected=${this.outpost?.type === OutpostTypeEnum.Ldap}>${t`LDAP`}</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
|
@ -88,6 +89,16 @@ export class OutpostForm extends Form<Outpost> {
|
|||
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>${provider.verboseName} ${provider.name}</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
${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`<option value=${ifDefined(provider.pk)} ?selected=${selected}>${provider.verboseName} ${provider.name}</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<LDAPProvider> {
|
||||
|
||||
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<LDAPProvider> => {
|
||||
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`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Name`}
|
||||
?required=${true}
|
||||
name="name">
|
||||
<input type="text" value="${ifDefined(this.provider?.name)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Bind flow`}
|
||||
?required=${true}
|
||||
name="authorizationFlow">
|
||||
<select class="pf-c-form-control">
|
||||
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowDesignationEnum.Authentication,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
return html`<option value=${ifDefined(flow.pk)} ?selected=${this.provider?.authorizationFlow === flow.pk}>${flow.name} (${flow.slug})</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Group`}
|
||||
name="searchGroup">
|
||||
<select class="pf-c-form-control">
|
||||
<option value="" ?selected=${this.provider?.searchGroup === undefined}>---------</option>
|
||||
${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => {
|
||||
return groups.results.map(group => {
|
||||
return html`<option value=${ifDefined(group.pk)} ?selected=${this.provider?.searchGroup === group.pk}>${group.name}</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Users in the selected group can do search queries.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header">
|
||||
${t`Protocol settings`}
|
||||
</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Base DN`}
|
||||
?required=${true}
|
||||
name="baseDn">
|
||||
<input type="text" value="${first(this.provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}" class="pf-c-form-control" required>
|
||||
<p class="pf-c-form__helper-text">${t`LDAP DN under which bind requests and search requests can be made.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -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`<ak-tabs>
|
||||
<section slot="page-overview" data-tab-title="${t`Overview`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list pf-m-3-col-on-lg">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Name`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${this.provider.name}</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Assigned to application`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-provider-related-application .provider=${this.provider}></ak-provider-related-application>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Base DN`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${this.provider.baseDn}</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">
|
||||
${t`Update`}
|
||||
</span>
|
||||
<span slot="header">
|
||||
${t`Update LDAP Provider`}
|
||||
</span>
|
||||
<ak-provider-ldap-form
|
||||
slot="form"
|
||||
.providerUUID=${this.provider.pk || 0}>
|
||||
</ak-provider-ldap-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${t`Edit`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section slot="page-changelog" data-tab-title="${t`Changelog`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.provider.pk || ""}
|
||||
targetModelApp="authentik_providers_ldap"
|
||||
targetModelName="LDAPProvider">
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
}
|
|
@ -156,6 +156,7 @@ export class OAuthSourceForm extends Form<OAuthSource> {
|
|||
<ak-form-element-horizontal
|
||||
label=${t`Consumer secret`}
|
||||
?required=${true}
|
||||
?writeOnly=${this.source !== undefined}
|
||||
name="consumerSecret">
|
||||
<input type="text" value="${ifDefined(this.source?.consumerSecret)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
|
|
Reference in New Issue