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:
Jens L 2021-05-05 10:40:56 +02:00 committed by GitHub
commit ca89201bd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1902 additions and 104 deletions

View File

@ -47,6 +47,7 @@ from authentik.policies.reputation.api import (
ReputationPolicyViewSet, ReputationPolicyViewSet,
UserReputationViewSet, UserReputationViewSet,
) )
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
from authentik.providers.oauth2.api.tokens import ( from authentik.providers.oauth2.api.tokens import (
@ -121,6 +122,7 @@ router.register(
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
) )
router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet) router.register("flows/instances", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet) router.register("flows/bindings", FlowStageBindingViewSet)
@ -151,6 +153,7 @@ router.register("policies/reputation/ips", IPReputationViewSet)
router.register("policies/reputation", ReputationPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet) router.register("providers/all", ProviderViewSet)
router.register("providers/ldap", LDAPProviderViewSet)
router.register("providers/proxy", ProxyProviderViewSet) router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet) router.register("providers/saml", SAMLProviderViewSet)

View File

@ -91,6 +91,23 @@ class ApplicationViewSet(ModelViewSet):
applications.append(application) applications.append(application)
return applications 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( @swagger_auto_schema(
manual_parameters=[ manual_parameters=[
openapi.Parameter( openapi.Parameter(

View File

@ -1,7 +1,9 @@
"""Groups API Viewset""" """Groups API Viewset"""
from django.db.models.query import QuerySet
from rest_framework.fields import JSONField from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.utils import is_dict from authentik.core.api.utils import is_dict
from authentik.core.models import Group from authentik.core.models import Group
@ -26,3 +28,16 @@ class GroupViewSet(ModelViewSet):
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]
filterset_fields = ["name", "is_superuser"] filterset_fields = ["name", "is_superuser"]
ordering = ["name"] 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)

View File

@ -1,6 +1,7 @@
"""User API Views""" """User API Views"""
from json import loads from json import loads
from django.db.models.query import QuerySet
from django.http.response import Http404 from django.http.response import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.http import urlencode 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.fields import CharField, JSONField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response 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.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required 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.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,
@ -33,6 +41,7 @@ class UserSerializer(ModelSerializer):
is_superuser = BooleanField(read_only=True) is_superuser = BooleanField(read_only=True)
avatar = CharField(read_only=True) avatar = CharField(read_only=True)
attributes = JSONField(validators=[is_dict], required=False) attributes = JSONField(validators=[is_dict], required=False)
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
class Meta: class Meta:
@ -44,6 +53,7 @@ class UserSerializer(ModelSerializer):
"is_active", "is_active",
"last_login", "last_login",
"is_superuser", "is_superuser",
"groups",
"email", "email",
"avatar", "avatar",
"attributes", "attributes",
@ -177,3 +187,16 @@ class UserViewSet(ModelViewSet):
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
) )
return Response({"link": link}) 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)

View File

@ -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",
},
],
},
)

View File

@ -50,4 +50,4 @@ class NotificationViewSet(
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()
return Notification.objects.filter(user=user) return Notification.objects.filter(user=user.pk)

View File

@ -86,6 +86,12 @@ class ConfigLoader:
url = urlparse(value) url = urlparse(value)
if url.scheme == "env": if url.scheme == "env":
value = os.getenv(url.netloc, url.query) 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 return value
def update_from_file(self, path: str): def update_from_file(self, path: str):
@ -163,6 +169,7 @@ class ConfigLoader:
# Walk each component of the path # Walk each component of the path
path_parts = path.split(sep) path_parts = path.split(sep)
for comp in path_parts[:-1]: for comp in path_parts[:-1]:
# pyright: reportGeneralTypeIssues=false
if comp not in root: if comp not in root:
root[comp] = {} root[comp] = {}
root = root.get(comp) root = root.get(comp)

View File

@ -24,6 +24,7 @@ class OutpostSerializer(ModelSerializer):
fields = [ fields = [
"pk", "pk",
"name", "name",
"type",
"providers", "providers",
"providers_obj", "providers_obj",
"service_connection", "service_connection",

View File

@ -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"
),
),
]

View File

@ -5,6 +5,7 @@ from typing import Iterable, Optional, Union
from uuid import uuid4 from uuid import uuid4
from dacite import from_dict from dacite import from_dict
from django.contrib.auth.models import Permission
from django.core.cache import cache from django.core.cache import cache
from django.db import models, transaction from django.db import models, transaction
from django.db.models.base import Model from django.db.models.base import Model
@ -64,7 +65,7 @@ class OutpostConfig:
class OutpostModel(Model): class OutpostModel(Model):
"""Base model for providers that need more objects than just themselves""" """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 a list of all required objects"""
return [self] return [self]
@ -77,6 +78,7 @@ class OutpostType(models.TextChoices):
"""Outpost types, currently only the reverse proxy is available""" """Outpost types, currently only the reverse proxy is available"""
PROXY = "proxy" PROXY = "proxy"
LDAP = "ldap"
def default_outpost_config(host: Optional[str] = None): def default_outpost_config(host: Optional[str] = None):
@ -334,9 +336,26 @@ class Outpost(models.Model):
# the ones the user needs # the ones the user needs
with transaction.atomic(): with transaction.atomic():
UserObjectPermission.objects.filter(user=user).delete() UserObjectPermission.objects.filter(user=user).delete()
for model in self.get_required_objects(): user.user_permissions.clear()
code_name = f"{model._meta.app_label}.view_{model._meta.model_name}" for model_or_perm in self.get_required_objects():
assign_perm(code_name, user, model) 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 return user
@property @property
@ -359,9 +378,9 @@ class Outpost(models.Model):
managed=f"goauthentik.io/outpost/{self.token_identifier}", 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""" """Get an iterator of all objects the user needs read access to"""
objects = [self] objects: list[Union[models.Model, str]] = [self]
for provider in ( for provider in (
Provider.objects.filter(outpost=self).select_related().select_subclasses() Provider.objects.filter(outpost=self).select_related().select_subclasses()
): ):

View File

@ -3,7 +3,7 @@ from os import R_OK, access
from os.path import expanduser from os.path import expanduser
from pathlib import Path from pathlib import Path
from socket import gethostname from socket import gethostname
from typing import Any from typing import Any, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
import yaml import yaml
@ -19,7 +19,7 @@ from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.reflection import path_to_class 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 ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
KubernetesServiceConnection, KubernetesServiceConnection,
@ -29,6 +29,8 @@ from authentik.outposts.models import (
OutpostState, OutpostState,
OutpostType, 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.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -36,6 +38,24 @@ from authentik.root.celery import CELERY_APP
LOGGER = get_logger() 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() @CELERY_APP.task()
def outpost_controller_all(): def outpost_controller_all():
"""Launch Controller for all Outposts which support it""" """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) outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
self.set_uid(slugify(outpost.name)) self.set_uid(slugify(outpost.name))
try: try:
if not outpost.service_connection: controller = controller_for_outpost(outpost)
if not controller:
return return
if outpost.type == OutpostType.PROXY: logs = controller.up_with_logs()
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()
LOGGER.debug("---------------Outpost Controller logs starting----------------") LOGGER.debug("---------------Outpost Controller logs starting----------------")
for log in logs: for log in logs:
LOGGER.debug(log) LOGGER.debug(log)
@ -100,12 +114,10 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
def outpost_pre_delete(outpost_pk: str): def outpost_pre_delete(outpost_pk: str):
"""Delete outpost objects before deleting the DB Object""" """Delete outpost objects before deleting the DB Object"""
outpost = Outpost.objects.get(pk=outpost_pk) outpost = Outpost.objects.get(pk=outpost_pk)
if outpost.type == OutpostType.PROXY: controller = controller_for_outpost(outpost)
service_connection = outpost.service_connection if not controller:
if isinstance(service_connection, DockerServiceConnection): return
ProxyDockerController(outpost, service_connection).down() controller.down()
if isinstance(service_connection, KubernetesServiceConnection):
ProxyKubernetesController(outpost, service_connection).down()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=MonitoredTask)

View File

View File

@ -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"]

View File

@ -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"

View File

@ -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"),
]

View File

@ -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"),
]

View File

@ -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),
),
]

View File

@ -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",
),
),
]

View File

@ -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")

View File

@ -42,7 +42,7 @@ class AuthorizationCodeViewSet(
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser: if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user) return super().get_queryset().filter(user=user.pk)
class RefreshTokenViewSet( class RefreshTokenViewSet(
@ -62,4 +62,4 @@ class RefreshTokenViewSet(
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser: if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user) return super().get_queryset().filter(user=user.pk)

View File

@ -1,11 +1,11 @@
"""authentik auth oauth provider app config""" """authentik oauth provider app config"""
from importlib import import_module from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
class AuthentikProviderOAuth2Config(AppConfig): class AuthentikProviderOAuth2Config(AppConfig):
"""authentik auth oauth provider app config""" """authentik oauth provider app config"""
name = "authentik.providers.oauth2" name = "authentik.providers.oauth2"
label = "authentik_providers_oauth2" label = "authentik_providers_oauth2"

View File

@ -1,7 +1,7 @@
"""authentik proxy models""" """authentik proxy models"""
import string import string
from random import SystemRandom from random import SystemRandom
from typing import Iterable, Optional, Type from typing import Iterable, Optional, Type, Union
from urllib.parse import urljoin from urllib.parse import urljoin
from django.db import models from django.db import models
@ -147,7 +147,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def __str__(self): def __str__(self):
return f"Proxy Provider {self.name}" 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] required_models = [self]
if self.certificate is not None: if self.certificate is not None:
required_models.append(self.certificate) required_models.append(self.certificate)

View File

@ -102,6 +102,7 @@ INSTALLED_APPS = [
"authentik.policies.password", "authentik.policies.password",
"authentik.policies.reputation", "authentik.policies.reputation",
"authentik.providers.proxy", "authentik.providers.proxy",
"authentik.providers.ldap",
"authentik.providers.oauth2", "authentik.providers.oauth2",
"authentik.providers.saml", "authentik.providers.saml",
"authentik.recovery", "authentik.recovery",

View File

@ -30,4 +30,4 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet):
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser: if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user) return super().get_queryset().filter(user=user.pk)

View File

@ -46,7 +46,7 @@ class StaticDeviceViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() 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): class StaticAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -49,7 +49,7 @@ class TOTPDeviceViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() 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): class TOTPAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -48,7 +48,7 @@ class WebAuthnDeviceViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() 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): class WebAuthnAdminDeviceViewSet(ReadOnlyModelViewSet):

View File

@ -54,4 +54,4 @@ class UserConsentViewSet(
user = self.request.user if self.request else get_anonymous_user() user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser: if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user) return super().get_queryset().filter(user=user.pk)

View File

@ -51,6 +51,7 @@ def send_mail(
try: try:
backend = stage.backend backend = stage.backend
except ValueError as exc: except ValueError as exc:
# pyright: reportGeneralTypeIssues=false
LOGGER.warning(exc) LOGGER.warning(exc)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
return return

View File

@ -100,7 +100,7 @@ stages:
versionSpec: '3.9' versionSpec: '3.9'
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:
script: npm install -g pyright@1.1.109 script: npm install -g pyright@1.1.136
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:
script: | script: |
@ -262,6 +262,9 @@ stages:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
versionSpec: '3.9' versionSpec: '3.9'
- task: NodeTool@0
inputs:
versionSpec: '16.x'
- task: DockerCompose@0 - task: DockerCompose@0
displayName: Run services displayName: Run services
inputs: inputs:
@ -379,7 +382,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '14.x' versionSpec: '16.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:

View File

@ -51,9 +51,9 @@ stages:
script: | script: |
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.39.0 golangci-lint run -v --timeout 200s docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.39.0 golangci-lint run -v --timeout 200s
workingDirectory: 'outpost/' workingDirectory: 'outpost/'
- stage: proxy_build_go - stage: build_go
jobs: jobs:
- job: build_go - job: proxy_build_go
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
steps: steps:
@ -70,9 +70,26 @@ stages:
command: 'build' command: 'build'
arguments: './cmd/proxy' arguments: './cmd/proxy'
workingDirectory: 'outpost/' 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: jobs:
- job: build - job: proxy_build_docker
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
steps: steps:
@ -97,3 +114,28 @@ stages:
Dockerfile: 'outpost/proxy.Dockerfile' Dockerfile: 'outpost/proxy.Dockerfile'
buildContext: 'outpost/' buildContext: 'outpost/'
tags: "gh-$(branchName)" 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)"

View File

@ -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)
}
}

View File

@ -5,7 +5,9 @@ go 1.14
require ( require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/coreos/go-oidc v2.2.1+incompatible 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/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/analysis v0.20.1 // indirect
github.com/go-openapi/errors v0.20.0 github.com/go-openapi/errors v0.20.0
github.com/go-openapi/runtime v0.19.28 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/swag v0.19.15
github.com/go-openapi/validate v0.20.2 github.com/go-openapi/validate v0.20.2
github.com/go-redis/redis/v7 v7.4.0 // indirect 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/golang/protobuf v1.5.2 // indirect
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
@ -20,6 +23,8 @@ require (
github.com/kr/pretty v0.2.1 // indirect github.com/kr/pretty v0.2.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // 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/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc
github.com/pelletier/go-toml v1.9.0 // indirect github.com/pelletier/go-toml v1.9.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -32,9 +37,9 @@ require (
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.1 // indirect github.com/spf13/viper v1.7.1 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // 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/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 google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect

View File

@ -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= 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= 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/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 h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw=
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 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/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/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 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 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 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 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/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-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/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-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 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 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-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-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-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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= 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.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 h1:Sxpo9PjEHDzhs3FbnGNonvDgWcMW2U7wGTcDDSFSceM=
github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 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.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.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 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.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.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.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 h1:9lYu6axek8LJrVkMVViVirRcpoaCxXX7+sSvmizGVnA=
github.com/go-openapi/runtime v0.19.28/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= 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= 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-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 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 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/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.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 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/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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 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/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/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/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 h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= 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= 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/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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 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= 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.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.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 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 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 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= 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.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/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 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 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/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= 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/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.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 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.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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/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.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.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 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 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= 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 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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/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.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 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= 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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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.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-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-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= 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-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-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-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-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 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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.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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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-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-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-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-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-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-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-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-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-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-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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/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/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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/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-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-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

13
outpost/ldap.Dockerfile Normal file
View File

@ -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"]

View File

@ -31,7 +31,7 @@ func doGlobalSetup(config map[string]interface{}) {
default: default:
log.SetLevel(log.DebugLevel) 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 var dsn string
if config[ConfigErrorReportingEnabled].(bool) { if config[ConfigErrorReportingEnabled].(bool) {

50
outpost/pkg/ldap/api.go Normal file
View File

@ -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
}

19
outpost/pkg/ldap/bind.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

58
outpost/pkg/ldap/ldap.go Normal file
View File

@ -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
}

View File

@ -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")
}

35
outpost/pkg/ldap/utils.go Normal file
View File

@ -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)
}

View File

@ -6,7 +6,6 @@ COPY . .
RUN go build -o /work/proxy ./cmd/proxy RUN go build -o /work/proxy ./cmd/proxy
# Copy binary to alpine
FROM gcr.io/distroless/base-debian10:debug FROM gcr.io/distroless/base-debian10:debug
COPY --from=builder /work/proxy / COPY --from=builder /work/proxy /

View File

@ -1329,6 +1329,33 @@ paths:
type: string type: string
format: slug format: slug
pattern: ^[-a-zA-Z0-9_]+$ 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/: /core/applications/{slug}/metrics/:
get: get:
operationId: core_applications_metrics operationId: core_applications_metrics
@ -4649,6 +4676,103 @@ paths:
required: true required: true
type: string type: string
format: uuid 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/: /outposts/outposts/:
get: get:
operationId: outposts_outposts_list operationId: outposts_outposts_list
@ -8717,6 +8841,203 @@ paths:
description: A unique integer value identifying this provider. description: A unique integer value identifying this provider.
required: true required: true
type: integer 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/: /providers/oauth2/:
get: get:
operationId: providers_oauth2_list operationId: providers_oauth2_list
@ -15056,6 +15377,11 @@ definitions:
title: Is superuser title: Is superuser
type: boolean type: boolean
readOnly: true readOnly: true
groups:
type: array
items:
$ref: '#/definitions/Group'
readOnly: true
email: email:
title: Email address title: Email address
type: string type: string
@ -15881,6 +16207,12 @@ definitions:
title: Name title: Name
type: string type: string
minLength: 1 minLength: 1
type:
title: Type
type: string
enum:
- proxy
- ldap
providers: providers:
type: array type: array
items: items:
@ -15934,6 +16266,41 @@ definitions:
title: Version outdated title: Version outdated
type: boolean type: boolean
readOnly: true 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: OpenIDConnectConfiguration:
description: Embed OpenID Connect provider information description: Embed OpenID Connect provider information
required: required:
@ -16442,6 +16809,7 @@ definitions:
- authentik.policies.password - authentik.policies.password
- authentik.policies.reputation - authentik.policies.reputation
- authentik.providers.proxy - authentik.providers.proxy
- authentik.providers.ldap
- authentik.providers.oauth2 - authentik.providers.oauth2
- authentik.providers.saml - authentik.providers.saml
- authentik.recovery - authentik.recovery
@ -16947,6 +17315,63 @@ definitions:
description: Description shown to the user when consenting. If left empty, description: Description shown to the user when consenting. If left empty,
the user won't be informed. the user won't be informed.
type: string 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: OAuth2ProviderSetupURLs:
type: object type: object
properties: properties:

View File

@ -12,7 +12,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '14.x' versionSpec: '16.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:
@ -31,7 +31,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '14.x' versionSpec: '16.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
@ -53,7 +53,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '14.x' versionSpec: '16.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
@ -77,7 +77,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '14.x' versionSpec: '16.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:

View File

@ -87,5 +87,6 @@
"typescript": "^4.2.4", "typescript": "^4.2.4",
"webcomponent-qr-code": "^1.0.5", "webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2" "yaml": "^1.10.2"
} },
"devDependencies": {}
} }

View File

@ -11,6 +11,7 @@ export interface PlexResource {
name: string; name: string;
provides: string; provides: string;
clientIdentifier: string; clientIdentifier: string;
owned: boolean;
} }
export const DEFAULT_HEADERS = { export const DEFAULT_HEADERS = {
@ -88,7 +89,7 @@ export class PlexAPIClient {
}); });
const resources: PlexResource[] = await resourcesResponse.json(); const resources: PlexResource[] = await resourcesResponse.json();
return resources.filter(r => { return resources.filter(r => {
return r.provides === "server"; return r.provides.toLowerCase().includes("server") && r.owned;
}); });
} }

View File

@ -91,7 +91,7 @@ msgid "Action"
msgstr "Action" msgstr "Action"
#: src/pages/groups/MemberSelectModal.ts:46 #: src/pages/groups/MemberSelectModal.ts:46
#: src/pages/users/UserListPage.ts:51 #: src/pages/users/UserListPage.ts:55
#: src/pages/users/UserViewPage.ts:116 #: src/pages/users/UserViewPage.ts:116
msgid "Active" msgid "Active"
msgstr "Active" msgstr "Active"
@ -767,8 +767,8 @@ msgstr "Copy Key"
#: src/pages/stages/prompt/PromptStageForm.ts:98 #: src/pages/stages/prompt/PromptStageForm.ts:98
#: src/pages/user-settings/tokens/UserTokenList.ts:50 #: src/pages/user-settings/tokens/UserTokenList.ts:50
#: src/pages/user-settings/tokens/UserTokenList.ts:58 #: src/pages/user-settings/tokens/UserTokenList.ts:58
#: src/pages/users/UserListPage.ts:151 #: src/pages/users/UserListPage.ts:155
#: src/pages/users/UserListPage.ts:159 #: src/pages/users/UserListPage.ts:163
msgid "Create" msgid "Create"
msgstr "Create" msgstr "Create"
@ -838,7 +838,7 @@ msgstr "Create Stage binding"
msgid "Create Token" msgid "Create Token"
msgstr "Create Token" msgstr "Create Token"
#: src/pages/users/UserListPage.ts:154 #: src/pages/users/UserListPage.ts:158
msgid "Create User" msgid "Create User"
msgstr "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/tokens/TokenListPage.ts:68
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40 #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40
#: src/pages/user-settings/tokens/UserTokenList.ts:125 #: src/pages/user-settings/tokens/UserTokenList.ts:125
#: src/pages/users/UserListPage.ts:115 #: src/pages/users/UserListPage.ts:119
msgid "Delete" msgid "Delete"
msgstr "Delete" msgstr "Delete"
@ -1006,8 +1006,8 @@ msgstr "Digest algorithm"
msgid "Digits" msgid "Digits"
msgstr "Digits" msgstr "Digits"
#: src/pages/users/UserListPage.ts:81 #: src/pages/users/UserListPage.ts:85
#: src/pages/users/UserListPage.ts:100 #: src/pages/users/UserListPage.ts:104
msgid "Disable" msgid "Disable"
msgstr "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/StageListPage.ts:98
#: src/pages/stages/prompt/PromptListPage.ts:75 #: src/pages/stages/prompt/PromptListPage.ts:75
#: src/pages/user-settings/tokens/UserTokenList.ts:113 #: 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 #: src/pages/users/UserViewPage.ts:147
msgid "Edit" msgid "Edit"
msgstr "Edit" msgstr "Edit"
@ -1119,8 +1119,8 @@ msgstr "Email or username"
msgid "Email: Text field with Email type." msgid "Email: Text field with Email type."
msgstr "Email: Text field with Email type." msgstr "Email: Text field with Email type."
#: src/pages/users/UserListPage.ts:81 #: src/pages/users/UserListPage.ts:85
#: src/pages/users/UserListPage.ts:100 #: src/pages/users/UserListPage.ts:104
msgid "Enable" msgid "Enable"
msgstr "Enable" msgstr "Enable"
@ -1520,6 +1520,10 @@ msgstr "Hidden: Hidden field, can be used to insert data into form."
msgid "Hide managed mappings" msgid "Hide managed mappings"
msgstr "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/events/RuleForm.ts:93
#: src/pages/groups/GroupForm.ts:131 #: src/pages/groups/GroupForm.ts:131
#: src/pages/outposts/OutpostForm.ts:98 #: 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." 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." 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" msgid "Impersonate"
msgstr "Impersonate" msgstr "Impersonate"
@ -1680,7 +1684,7 @@ msgid "Label shown next to/above the prompt."
msgstr "Label shown next to/above the prompt." msgstr "Label shown next to/above the prompt."
#: src/pages/groups/MemberSelectModal.ts:47 #: src/pages/groups/MemberSelectModal.ts:47
#: src/pages/users/UserListPage.ts:52 #: src/pages/users/UserListPage.ts:56
#: src/pages/users/UserViewPage.ts:108 #: src/pages/users/UserViewPage.ts:108
msgid "Last login" msgid "Last login"
msgstr "Last login" msgstr "Last login"
@ -1986,7 +1990,7 @@ msgstr "Monitor"
#: src/pages/stages/user_write/UserWriteStageForm.ts:55 #: src/pages/stages/user_write/UserWriteStageForm.ts:55
#: src/pages/user-settings/UserDetailsPage.ts:64 #: src/pages/user-settings/UserDetailsPage.ts:64
#: src/pages/users/UserForm.ts:54 #: src/pages/users/UserForm.ts:54
#: src/pages/users/UserListPage.ts:50 #: src/pages/users/UserListPage.ts:54
#: src/pages/users/UserViewPage.ts:92 #: src/pages/users/UserViewPage.ts:92
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
@ -2020,7 +2024,7 @@ msgstr "New version available!"
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:108 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:108
#: src/pages/tokens/TokenListPage.ts:56 #: src/pages/tokens/TokenListPage.ts:56
#: src/pages/user-settings/tokens/UserTokenList.ts:83 #: src/pages/user-settings/tokens/UserTokenList.ts:83
#: src/pages/users/UserListPage.ts:63 #: src/pages/users/UserListPage.ts:67
msgid "No" msgid "No"
msgstr "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." msgid "No policies cached. Users may experience slow response times."
msgstr "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." msgid "No recovery flow is configured."
msgstr "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." msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
msgstr "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 #: src/pages/users/UserViewPage.ts:165
msgid "Reset Password" msgid "Reset Password"
msgstr "Reset Password" msgstr "Reset Password"
@ -3172,7 +3176,7 @@ msgstr "Successfully deleted {0} {1}"
msgid "Successfully generated certificate-key pair." msgid "Successfully generated certificate-key pair."
msgstr "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 #: src/pages/users/UserViewPage.ts:160
msgid "Successfully generated recovery link" msgid "Successfully generated recovery link"
msgstr "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/settings/UserSettingsAuthenticatorWebAuthn.ts:71
#: src/pages/user-settings/tokens/UserTokenList.ts:105 #: src/pages/user-settings/tokens/UserTokenList.ts:105
#: src/pages/users/UserActiveForm.ts:66 #: src/pages/users/UserActiveForm.ts:66
#: src/pages/users/UserListPage.ts:68 #: src/pages/users/UserListPage.ts:72
#: src/pages/users/UserViewPage.ts:139 #: src/pages/users/UserViewPage.ts:139
msgid "Update" msgid "Update"
msgstr "Update" msgstr "Update"
@ -3693,7 +3697,7 @@ msgid "Update Token"
msgstr "Update Token" msgstr "Update Token"
#: src/pages/policies/BoundPoliciesList.ts:106 #: src/pages/policies/BoundPoliciesList.ts:106
#: src/pages/users/UserListPage.ts:71 #: src/pages/users/UserListPage.ts:75
#: src/pages/users/UserViewPage.ts:142 #: src/pages/users/UserViewPage.ts:142
msgid "Update User" msgid "Update User"
msgstr "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/property-mappings/PropertyMappingTestForm.ts:51
#: src/pages/tokens/TokenListPage.ts:45 #: src/pages/tokens/TokenListPage.ts:45
#: src/pages/user-settings/tokens/UserTokenList.ts:72 #: src/pages/user-settings/tokens/UserTokenList.ts:72
#: src/pages/users/UserListPage.ts:88 #: src/pages/users/UserListPage.ts:92
#: src/pages/users/UserListPage.ts:108 #: src/pages/users/UserListPage.ts:112
msgid "User" msgid "User"
msgstr "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/interfaces/AdminInterface.ts:32
#: src/pages/admin-overview/AdminOverviewPage.ts:50 #: src/pages/admin-overview/AdminOverviewPage.ts:50
#: src/pages/users/UserListPage.ts:32 #: src/pages/users/UserListPage.ts:33
msgid "Users" msgid "Users"
msgstr "Users" msgstr "Users"
@ -4013,7 +4017,7 @@ msgstr "X509 Subject"
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:105 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:105
#: src/pages/tokens/TokenListPage.ts:56 #: src/pages/tokens/TokenListPage.ts:56
#: src/pages/user-settings/tokens/UserTokenList.ts:83 #: src/pages/user-settings/tokens/UserTokenList.ts:83
#: src/pages/users/UserListPage.ts:63 #: src/pages/users/UserListPage.ts:67
msgid "Yes" msgid "Yes"
msgstr "Yes" msgstr "Yes"

View File

@ -91,7 +91,7 @@ msgid "Action"
msgstr "" msgstr ""
#: src/pages/groups/MemberSelectModal.ts:46 #: src/pages/groups/MemberSelectModal.ts:46
#: src/pages/users/UserListPage.ts:51 #: src/pages/users/UserListPage.ts:55
#: src/pages/users/UserViewPage.ts:116 #: src/pages/users/UserViewPage.ts:116
msgid "Active" msgid "Active"
msgstr "" msgstr ""
@ -761,8 +761,8 @@ msgstr ""
#: src/pages/stages/prompt/PromptStageForm.ts:98 #: src/pages/stages/prompt/PromptStageForm.ts:98
#: src/pages/user-settings/tokens/UserTokenList.ts:50 #: src/pages/user-settings/tokens/UserTokenList.ts:50
#: src/pages/user-settings/tokens/UserTokenList.ts:58 #: src/pages/user-settings/tokens/UserTokenList.ts:58
#: src/pages/users/UserListPage.ts:151 #: src/pages/users/UserListPage.ts:155
#: src/pages/users/UserListPage.ts:159 #: src/pages/users/UserListPage.ts:163
msgid "Create" msgid "Create"
msgstr "" msgstr ""
@ -832,7 +832,7 @@ msgstr ""
msgid "Create Token" msgid "Create Token"
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:154 #: src/pages/users/UserListPage.ts:158
msgid "Create User" msgid "Create User"
msgstr "" msgstr ""
@ -910,7 +910,7 @@ msgstr ""
#: src/pages/tokens/TokenListPage.ts:68 #: src/pages/tokens/TokenListPage.ts:68
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40 #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:40
#: src/pages/user-settings/tokens/UserTokenList.ts:125 #: src/pages/user-settings/tokens/UserTokenList.ts:125
#: src/pages/users/UserListPage.ts:115 #: src/pages/users/UserListPage.ts:119
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@ -998,8 +998,8 @@ msgstr ""
msgid "Digits" msgid "Digits"
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:81 #: src/pages/users/UserListPage.ts:85
#: src/pages/users/UserListPage.ts:100 #: src/pages/users/UserListPage.ts:104
msgid "Disable" msgid "Disable"
msgstr "" msgstr ""
@ -1060,7 +1060,7 @@ msgstr ""
#: src/pages/stages/StageListPage.ts:98 #: src/pages/stages/StageListPage.ts:98
#: src/pages/stages/prompt/PromptListPage.ts:75 #: src/pages/stages/prompt/PromptListPage.ts:75
#: src/pages/user-settings/tokens/UserTokenList.ts:113 #: 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 #: src/pages/users/UserViewPage.ts:147
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -1111,8 +1111,8 @@ msgstr ""
msgid "Email: Text field with Email type." msgid "Email: Text field with Email type."
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:81 #: src/pages/users/UserListPage.ts:85
#: src/pages/users/UserListPage.ts:100 #: src/pages/users/UserListPage.ts:104
msgid "Enable" msgid "Enable"
msgstr "" msgstr ""
@ -1512,6 +1512,10 @@ msgstr ""
msgid "Hide managed mappings" msgid "Hide managed mappings"
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:186
msgid "Hide service-accounts"
msgstr ""
#: src/pages/events/RuleForm.ts:93 #: src/pages/events/RuleForm.ts:93
#: src/pages/groups/GroupForm.ts:131 #: src/pages/groups/GroupForm.ts:131
#: src/pages/outposts/OutpostForm.ts:98 #: 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." msgid "If your authentik Instance is using a self-signed certificate, set this value."
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:143 #: src/pages/users/UserListPage.ts:147
msgid "Impersonate" msgid "Impersonate"
msgstr "" msgstr ""
@ -1672,7 +1676,7 @@ msgid "Label shown next to/above the prompt."
msgstr "" msgstr ""
#: src/pages/groups/MemberSelectModal.ts:47 #: src/pages/groups/MemberSelectModal.ts:47
#: src/pages/users/UserListPage.ts:52 #: src/pages/users/UserListPage.ts:56
#: src/pages/users/UserViewPage.ts:108 #: src/pages/users/UserViewPage.ts:108
msgid "Last login" msgid "Last login"
msgstr "" msgstr ""
@ -1978,7 +1982,7 @@ msgstr ""
#: src/pages/stages/user_write/UserWriteStageForm.ts:55 #: src/pages/stages/user_write/UserWriteStageForm.ts:55
#: src/pages/user-settings/UserDetailsPage.ts:64 #: src/pages/user-settings/UserDetailsPage.ts:64
#: src/pages/users/UserForm.ts:54 #: src/pages/users/UserForm.ts:54
#: src/pages/users/UserListPage.ts:50 #: src/pages/users/UserListPage.ts:54
#: src/pages/users/UserViewPage.ts:92 #: src/pages/users/UserViewPage.ts:92
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -2012,7 +2016,7 @@ msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:108 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:108
#: src/pages/tokens/TokenListPage.ts:56 #: src/pages/tokens/TokenListPage.ts:56
#: src/pages/user-settings/tokens/UserTokenList.ts:83 #: src/pages/user-settings/tokens/UserTokenList.ts:83
#: src/pages/users/UserListPage.ts:63 #: src/pages/users/UserListPage.ts:67
msgid "No" msgid "No"
msgstr "" msgstr ""
@ -2062,7 +2066,7 @@ msgstr ""
msgid "No policies cached. Users may experience slow response times." msgid "No policies cached. Users may experience slow response times."
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:135 #: src/pages/users/UserListPage.ts:139
msgid "No recovery flow is configured." msgid "No recovery flow is configured."
msgstr "" msgstr ""
@ -2620,7 +2624,7 @@ msgstr ""
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:140 #: src/pages/users/UserListPage.ts:144
#: src/pages/users/UserViewPage.ts:165 #: src/pages/users/UserViewPage.ts:165
msgid "Reset Password" msgid "Reset Password"
msgstr "" msgstr ""
@ -3164,7 +3168,7 @@ msgstr ""
msgid "Successfully generated certificate-key pair." msgid "Successfully generated certificate-key pair."
msgstr "" msgstr ""
#: src/pages/users/UserListPage.ts:128 #: src/pages/users/UserListPage.ts:132
#: src/pages/users/UserViewPage.ts:160 #: src/pages/users/UserViewPage.ts:160
msgid "Successfully generated recovery link" msgid "Successfully generated recovery link"
msgstr "" msgstr ""
@ -3601,7 +3605,7 @@ msgstr ""
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:71 #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts:71
#: src/pages/user-settings/tokens/UserTokenList.ts:105 #: src/pages/user-settings/tokens/UserTokenList.ts:105
#: src/pages/users/UserActiveForm.ts:66 #: src/pages/users/UserActiveForm.ts:66
#: src/pages/users/UserListPage.ts:68 #: src/pages/users/UserListPage.ts:72
#: src/pages/users/UserViewPage.ts:139 #: src/pages/users/UserViewPage.ts:139
msgid "Update" msgid "Update"
msgstr "" msgstr ""
@ -3681,7 +3685,7 @@ msgid "Update Token"
msgstr "" msgstr ""
#: src/pages/policies/BoundPoliciesList.ts:106 #: src/pages/policies/BoundPoliciesList.ts:106
#: src/pages/users/UserListPage.ts:71 #: src/pages/users/UserListPage.ts:75
#: src/pages/users/UserViewPage.ts:142 #: src/pages/users/UserViewPage.ts:142
msgid "Update User" msgid "Update User"
msgstr "" msgstr ""
@ -3746,8 +3750,8 @@ msgstr ""
#: src/pages/property-mappings/PropertyMappingTestForm.ts:51 #: src/pages/property-mappings/PropertyMappingTestForm.ts:51
#: src/pages/tokens/TokenListPage.ts:45 #: src/pages/tokens/TokenListPage.ts:45
#: src/pages/user-settings/tokens/UserTokenList.ts:72 #: src/pages/user-settings/tokens/UserTokenList.ts:72
#: src/pages/users/UserListPage.ts:88 #: src/pages/users/UserListPage.ts:92
#: src/pages/users/UserListPage.ts:108 #: src/pages/users/UserListPage.ts:112
msgid "User" msgid "User"
msgstr "" msgstr ""
@ -3829,7 +3833,7 @@ msgstr ""
#: src/interfaces/AdminInterface.ts:32 #: src/interfaces/AdminInterface.ts:32
#: src/pages/admin-overview/AdminOverviewPage.ts:50 #: src/pages/admin-overview/AdminOverviewPage.ts:50
#: src/pages/users/UserListPage.ts:32 #: src/pages/users/UserListPage.ts:33
msgid "Users" msgid "Users"
msgstr "" msgstr ""
@ -3999,7 +4003,7 @@ msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:105 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:105
#: src/pages/tokens/TokenListPage.ts:56 #: src/pages/tokens/TokenListPage.ts:56
#: src/pages/user-settings/tokens/UserTokenList.ts:83 #: src/pages/user-settings/tokens/UserTokenList.ts:83
#: src/pages/users/UserListPage.ts:63 #: src/pages/users/UserListPage.ts:67
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View File

@ -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 { t } from "@lingui/macro";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
@ -50,7 +50,8 @@ export class OutpostForm extends Form<Outpost> {
?required=${true} ?required=${true}
name="type"> name="type">
<select class="pf-c-form-control"> <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> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<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>`; return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>${provider.verboseName} ${provider.name}</option>`;
}); });
}), html`<option>${t`Loading...`}</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> </select>
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p> <p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>

View File

@ -8,6 +8,7 @@ import "../../elements/buttons/Dropdown";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "../../elements/forms/ModalForm"; import "../../elements/forms/ModalForm";
import "../../elements/forms/ProxyForm"; import "../../elements/forms/ProxyForm";
import "./ldap/LDAPProviderForm";
import "./oauth2/OAuth2ProviderForm"; import "./oauth2/OAuth2ProviderForm";
import "./proxy/ProxyProviderForm"; import "./proxy/ProxyProviderForm";
import "./saml/SAMLProviderForm"; import "./saml/SAMLProviderForm";

View File

@ -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>`;
}
}

View File

@ -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>`;
}
}

View File

@ -156,6 +156,7 @@ export class OAuthSourceForm extends Form<OAuthSource> {
<ak-form-element-horizontal <ak-form-element-horizontal
label=${t`Consumer secret`} label=${t`Consumer secret`}
?required=${true} ?required=${true}
?writeOnly=${this.source !== undefined}
name="consumerSecret"> name="consumerSecret">
<input type="text" value="${ifDefined(this.source?.consumerSecret)}" class="pf-c-form-control" required> <input type="text" value="${ifDefined(this.source?.consumerSecret)}" class="pf-c-form-control" required>
</ak-form-element-horizontal> </ak-form-element-horizontal>