From ff0d3c3d639aafc62bdd4b20ff1673b85e96c981 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 8 Jul 2023 01:15:35 +0200 Subject: [PATCH 1/6] sources/ldap: fix page size (#6187) Signed-off-by: Jens Langhammer --- authentik/lib/default.yml | 1 + authentik/sources/ldap/sync/base.py | 3 ++- website/docs/installation/configuration.md | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 0d1f15b62..45ffb3073 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -73,6 +73,7 @@ outposts: ldap: task_timeout_hours: 2 + page_size: 50 tls: ciphers: null diff --git a/authentik/sources/ldap/sync/base.py b/authentik/sources/ldap/sync/base.py index 235c7be26..97b1c381a 100644 --- a/authentik/sources/ldap/sync/base.py +++ b/authentik/sources/ldap/sync/base.py @@ -9,6 +9,7 @@ from structlog.stdlib import BoundLogger, get_logger from authentik.core.exceptions import PropertyMappingExpressionException from authentik.events.models import Event, EventAction +from authentik.lib.config import CONFIG from authentik.lib.merge import MERGE_LIST_UNIQUE from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource @@ -92,7 +93,7 @@ class BaseLDAPSynchronizer: types_only=False, get_operational_attributes=False, controls=None, - paged_size=5, + paged_size=int(CONFIG.y("ldap.page_size", 50)), paged_criticality=False, ): """Search in pages, returns each page""" diff --git a/website/docs/installation/configuration.md b/website/docs/installation/configuration.md index dd3fceb0e..71d83fe13 100644 --- a/website/docs/installation/configuration.md +++ b/website/docs/installation/configuration.md @@ -277,6 +277,16 @@ Timeout in hours for LDAP synchronization tasks. Defaults to `2`. +### `AUTHENTIK_LDAP__PAGE_SIZE` + +:::info +Requires authentik 2023.6.1 +::: + +Page size for LDAP synchronization. Controls the number of objects created in a single task. + +Defaults to `50`. + ### `AUTHENTIK_LDAP__TLS__CIPHERS` :::info From 5fe737326e226944f1582b28bd3d9339b55929d6 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 8 Jul 2023 02:32:47 +0200 Subject: [PATCH 2/6] sources/ldap: fix more errors (#6191) --- authentik/sources/ldap/tasks.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/authentik/sources/ldap/tasks.py b/authentik/sources/ldap/tasks.py index 04348188c..174b97e0d 100644 --- a/authentik/sources/ldap/tasks.py +++ b/authentik/sources/ldap/tasks.py @@ -30,12 +30,15 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/" def ldap_sync_all(): """Sync all sources""" for source in LDAPSource.objects.filter(enabled=True): - ldap_sync_single(source) + ldap_sync_single(source.pk) @CELERY_APP.task() -def ldap_sync_single(source: LDAPSource): +def ldap_sync_single(source_pk: str): """Sync a single source""" + source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first() + if not source: + return task = chain( # User and group sync can happen at once, they have no dependencies on each other group( @@ -71,9 +74,8 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_key: str): """Synchronization of an LDAP Source""" self.result_timeout_hours = int(CONFIG.y("ldap.task_timeout_hours")) - try: - source: LDAPSource = LDAPSource.objects.get(pk=source_pk) - except LDAPSource.DoesNotExist: + source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first() + if not source: # Because the source couldn't be found, we don't have a UID # to set the state with return From 935821857a6c8d6935b2cec33b43b94ac32cb640 Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 8 Jul 2023 20:51:05 +0200 Subject: [PATCH 3/6] outposts/ldap: add more tests (#6188) * outposts/ldap: add tests Signed-off-by: Jens Langhammer * fix missing posixAccount Signed-off-by: Jens Langhammer * attempt to expand attributes Signed-off-by: Jens Langhammer * fix routing without base DN Signed-off-by: Jens Langhammer * more logging Signed-off-by: Jens Langhammer * remove our custom attribute filtering since this is done by the ldap library Signed-off-by: Jens Langhammer * add test for schema Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- internal/outpost/ldap/entries.go | 16 +++++++--- internal/outpost/ldap/entries_test.go | 27 ++++++++++++++++ internal/outpost/ldap/ldap_test.go | 31 +++++++++++++++++++ internal/outpost/ldap/search.go | 7 ++--- internal/outpost/ldap/search/direct/direct.go | 2 +- internal/outpost/ldap/search/request.go | 16 ---------- internal/outpost/ldap/search_route.go | 29 ----------------- tests/e2e/test_provider_ldap.py | 24 ++++++++++++++ 8 files changed, 96 insertions(+), 56 deletions(-) create mode 100644 internal/outpost/ldap/entries_test.go create mode 100644 internal/outpost/ldap/ldap_test.go diff --git a/internal/outpost/ldap/entries.go b/internal/outpost/ldap/entries.go index f339bae3c..23bab252d 100644 --- a/internal/outpost/ldap/entries.go +++ b/internal/outpost/ldap/entries.go @@ -40,11 +40,17 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { "name": {u.Name}, "displayName": {u.Name}, "mail": {*u.Email}, - "objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser}, - "uidNumber": {pi.GetUidNumber(u)}, - "gidNumber": {pi.GetUidNumber(u)}, - "homeDirectory": {fmt.Sprintf("/home/%s", u.Username)}, - "sn": {u.Name}, + "objectClass": { + constants.OCUser, + constants.OCOrgPerson, + constants.OCInetOrgPerson, + constants.OCAKUser, + constants.OCPosixAccount, + }, + "uidNumber": {pi.GetUidNumber(u)}, + "gidNumber": {pi.GetUidNumber(u)}, + "homeDirectory": {fmt.Sprintf("/home/%s", u.Username)}, + "sn": {u.Name}, }) return &ldap.Entry{DN: dn, Attributes: attrs} } diff --git a/internal/outpost/ldap/entries_test.go b/internal/outpost/ldap/entries_test.go new file mode 100644 index 000000000..dc01f2d2c --- /dev/null +++ b/internal/outpost/ldap/entries_test.go @@ -0,0 +1,27 @@ +package ldap_test + +import ( + "testing" + + "beryju.io/ldap" + "github.com/stretchr/testify/assert" + "goauthentik.io/api/v3" +) + +func Test_UserEntry(t *testing.T) { + pi := ProviderInstance() + u := api.User{ + Username: "foo", + Name: "bar", + } + entry := pi.UserEntry(u) + assert.Equal(t, "cn=foo,ou=users,dc=ldap,dc=goauthentik,dc=io", entry.DN) + assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{ + Name: "cn", + Values: []string{u.Username}, + }) + assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{ + Name: "displayName", + Values: []string{u.Name}, + }) +} diff --git a/internal/outpost/ldap/ldap_test.go b/internal/outpost/ldap/ldap_test.go new file mode 100644 index 000000000..7b64d0c60 --- /dev/null +++ b/internal/outpost/ldap/ldap_test.go @@ -0,0 +1,31 @@ +package ldap_test + +import ( + "testing" + + "beryju.io/ldap" + "github.com/stretchr/testify/assert" + oldap "goauthentik.io/internal/outpost/ldap" +) + +func ProviderInstance() *oldap.ProviderInstance { + return &oldap.ProviderInstance{ + BaseDN: "dc=ldap,dc=goauthentik,dc=io", + UserDN: "ou=users,dc=ldap,dc=goauthentik,dc=io", + VirtualGroupDN: "ou=virtual-groups,dc=ldap,dc=goauthentik,dc=io", + GroupDN: "ou=groups,dc=ldap,dc=goauthentik,dc=io", + } +} + +func AssertLDAPAttributes(t *testing.T, attrs []*ldap.EntryAttribute, expected *ldap.EntryAttribute) { + found := false + for _, attr := range attrs { + if attr.Name == expected.Name { + assert.Equal(t, expected.Values, attr.Values) + found = true + } + } + if !found { + t.Fatalf("Key %s not found in ldap attributes", expected.Name) + } +} diff --git a/internal/outpost/ldap/search.go b/internal/outpost/ldap/search.go index 856b8ccbd..a082cd8e9 100644 --- a/internal/outpost/ldap/search.go +++ b/internal/outpost/ldap/search.go @@ -22,7 +22,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n "type": "search", "app": selectedApp, }).Observe(float64(span.EndTime.Sub(span.StartTime))) - req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request") + req.Log().WithField("attributes", searchReq.Attributes).WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request") }() defer func() { @@ -40,10 +40,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n } selectedApp = selectedProvider.GetAppSlug() result, err := ls.searchRoute(req, selectedProvider) - if err != nil { - return result, nil - } - return ls.filterResultAttributes(req, result), nil + return result, err } func (ls *LDAPServer) fallbackRootDSE(req *search.Request) (ldap.ServerSearchResult, error) { diff --git a/internal/outpost/ldap/search/direct/direct.go b/internal/outpost/ldap/search/direct/direct.go index b82b0ed09..7ac59f834 100644 --- a/internal/outpost/ldap/search/direct/direct.go +++ b/internal/outpost/ldap/search/direct/direct.go @@ -87,7 +87,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig()) c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter) - scope := req.SearchRequest.Scope + scope := req.Scope needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass) if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { diff --git a/internal/outpost/ldap/search/request.go b/internal/outpost/ldap/search/request.go index 7475c5d95..e44f36d4a 100644 --- a/internal/outpost/ldap/search/request.go +++ b/internal/outpost/ldap/search/request.go @@ -75,19 +75,3 @@ func (r *Request) Log() *log.Entry { func (r *Request) RemoteAddr() string { return utils.GetIP(r.conn.RemoteAddr()) } - -func (r *Request) FilterLDAPAttributes(res ldap.ServerSearchResult, cb func(attr *ldap.EntryAttribute) bool) ldap.ServerSearchResult { - for _, e := range res.Entries { - newAttrs := []*ldap.EntryAttribute{} - for _, attr := range e.Attributes { - include := cb(attr) - if include { - newAttrs = append(newAttrs, attr) - } else { - r.Log().WithField("key", attr.Name).Trace("filtering out field based on LDAP request") - } - } - e.Attributes = newAttrs - } - return res -} diff --git a/internal/outpost/ldap/search_route.go b/internal/outpost/ldap/search_route.go index c91982348..216f9c6a2 100644 --- a/internal/outpost/ldap/search_route.go +++ b/internal/outpost/ldap/search_route.go @@ -53,32 +53,3 @@ func (ls *LDAPServer) searchRoute(req *search.Request, pi *ProviderInstance) (ld req.Log().Trace("routing to default") return pi.searcher.Search(req) } - -func (ls *LDAPServer) filterResultAttributes(req *search.Request, result ldap.ServerSearchResult) ldap.ServerSearchResult { - allowedAttributes := []string{} - if len(req.Attributes) == 1 && req.Attributes[0] == constants.SearchAttributeNone { - allowedAttributes = []string{"objectClass"} - } - if len(req.Attributes) > 0 { - // Only strictly filter allowed attributes if we haven't already narrowed the attributes - // down - if len(allowedAttributes) < 1 { - allowedAttributes = req.Attributes - } - // Filter LDAP returned attributes by search requested attributes, taking "1.1" - // into consideration - return req.FilterLDAPAttributes(result, func(attr *ldap.EntryAttribute) bool { - for _, allowed := range allowedAttributes { - if allowed == constants.SearchAttributeAllUser || - allowed == constants.SearchAttributeAllOperational { - return true - } - if strings.EqualFold(allowed, attr.Name) { - return true - } - } - return false - }) - } - return result -} diff --git a/tests/e2e/test_provider_ldap.py b/tests/e2e/test_provider_ldap.py index 061ebef75..2045f1d0b 100644 --- a/tests/e2e/test_provider_ldap.py +++ b/tests/e2e/test_provider_ldap.py @@ -250,6 +250,7 @@ class TestProviderLDAP(SeleniumTestCase): "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user", + "posixAccount", ], "uidNumber": 2000 + o_user.pk, "gidNumber": 2000 + o_user.pk, @@ -277,6 +278,7 @@ class TestProviderLDAP(SeleniumTestCase): "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user", + "posixAccount", ], "uidNumber": 2000 + embedded_account.pk, "gidNumber": 2000 + embedded_account.pk, @@ -304,6 +306,7 @@ class TestProviderLDAP(SeleniumTestCase): "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user", + "posixAccount", ], "uidNumber": 2000 + self.user.pk, "gidNumber": 2000 + self.user.pk, @@ -320,3 +323,24 @@ class TestProviderLDAP(SeleniumTestCase): }, ], ) + + @retry() + @apply_blueprint( + "default/flow-default-authentication-flow.yaml", + "default/flow-default-invalidation-flow.yaml", + ) + @reconcile_app("authentik_outposts") + def test_ldap_schema(self): + """Test LDAP Schema""" + self._prepare() + server = Server("ldap://localhost:3389", get_info=ALL) + _connection = Connection( + server, + raise_exceptions=True, + user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", + password=self.user.username, + ) + _connection.bind() + self.assertIsNotNone(server.schema) + self.assertTrue(server.schema.is_valid()) + self.assertIsNotNone(server.schema.object_classes["goauthentik.io/ldap/user"]) From 622c0faebf385968722386c97a263709dfd518f7 Mon Sep 17 00:00:00 2001 From: risson <18313093+rissson@users.noreply.github.com> Date: Sat, 8 Jul 2023 21:16:43 +0200 Subject: [PATCH 4/6] outposts/ldap: add test for attribute filtering (#6189) add failing test case Signed-off-by: Marc 'risson' Schmitt Signed-off-by: Jens Langhammer --- tests/e2e/test_provider_ldap.py | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/e2e/test_provider_ldap.py b/tests/e2e/test_provider_ldap.py index 2045f1d0b..efabfd3b8 100644 --- a/tests/e2e/test_provider_ldap.py +++ b/tests/e2e/test_provider_ldap.py @@ -344,3 +344,76 @@ class TestProviderLDAP(SeleniumTestCase): self.assertIsNotNone(server.schema) self.assertTrue(server.schema.is_valid()) self.assertIsNotNone(server.schema.object_classes["goauthentik.io/ldap/user"]) + + @retry() + @apply_blueprint( + "default/flow-default-authentication-flow.yaml", + "default/flow-default-invalidation-flow.yaml", + ) + @reconcile_app("authentik_outposts") + def test_ldap_search_attrs_filter(self): + """Test search with attributes filtering""" + # Remove akadmin to ensure list is correct + # Remove user before starting container so it's not cached + User.objects.filter(username="akadmin").delete() + + outpost = self._prepare() + server = Server("ldap://localhost:3389", get_info=ALL) + _connection = Connection( + server, + raise_exceptions=True, + user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", + password=self.user.username, + ) + _connection.bind() + self.assertTrue( + Event.objects.filter( + action=EventAction.LOGIN, + user={ + "pk": self.user.pk, + "email": self.user.email, + "username": self.user.username, + }, + ) + ) + + embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user + + _connection.search( + "ou=Users,DC=ldaP,dc=goauthentik,dc=io", + "(objectClass=user)", + search_scope=SUBTREE, + attributes=["cn"], + ) + response: dict = _connection.response + # Remove raw_attributes to make checking easier + for obj in response: + del obj["raw_attributes"] + del obj["raw_dn"] + o_user = outpost.user + self.assertCountEqual( + response, + [ + { + "dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", + "attributes": { + "cn": o_user.username, + }, + "type": "searchResEntry", + }, + { + "dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io", + "attributes": { + "cn": embedded_account.username, + }, + "type": "searchResEntry", + }, + { + "dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", + "attributes": { + "cn": self.user.username, + }, + "type": "searchResEntry", + }, + ], + ) From 080ac6b5bbf4eed210745db00525f7de9db7a4d1 Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 10 Jul 2023 12:12:39 +0200 Subject: [PATCH 5/6] core: fix UUID filter field for users api (#6203) Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 108714fc3..123ae0fb9 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -15,7 +15,7 @@ from django.utils.http import urlencode from django.utils.text import slugify from django.utils.timezone import now from django.utils.translation import gettext as _ -from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter +from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter, UUIDFilter from django_filters.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( @@ -284,7 +284,7 @@ class UsersFilter(FilterSet): ) is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser") - uuid = CharFilter(field_name="uuid") + uuid = UUIDFilter(field_name="uuid") path = CharFilter( field_name="path", From d6af506a78caaf9e6ef394dffa1f931bcc2cd656 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 10 Jul 2023 13:20:22 +0200 Subject: [PATCH 6/6] release: 2023.6.1 --- .bumpversion.cfg | 2 +- authentik/__init__.py | 2 +- docker-compose.yml | 4 ++-- internal/constants/constants.go | 2 +- pyproject.toml | 2 +- schema.yml | 2 +- web/src/common/constants.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2332a36d2..cc4a5bc17 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2023.6.0 +current_version = 2023.6.1 tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/authentik/__init__.py b/authentik/__init__.py index d72f7dee7..3add9502d 100644 --- a/authentik/__init__.py +++ b/authentik/__init__.py @@ -2,7 +2,7 @@ from os import environ from typing import Optional -__version__ = "2023.6.0" +__version__ = "2023.6.1" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" diff --git a/docker-compose.yml b/docker-compose.yml index bb0bec3ac..16d873d0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: volumes: - redis:/data server: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.1} restart: unless-stopped command: server environment: @@ -53,7 +53,7 @@ services: - postgresql - redis worker: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.1} restart: unless-stopped command: worker environment: diff --git a/internal/constants/constants.go b/internal/constants/constants.go index f6f23ae6f..f47f80b29 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -29,4 +29,4 @@ func UserAgent() string { return fmt.Sprintf("authentik@%s", FullVersion()) } -const VERSION = "2023.6.0" +const VERSION = "2023.6.1" diff --git a/pyproject.toml b/pyproject.toml index de0b3a169..716da3b89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ filterwarnings = [ [tool.poetry] name = "authentik" -version = "2023.6.0" +version = "2023.6.1" description = "" authors = ["authentik Team "] diff --git a/schema.yml b/schema.yml index 12c005ce9..f85078279 100644 --- a/schema.yml +++ b/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: authentik - version: 2023.6.0 + version: 2023.6.1 description: Making authentication simple. contact: email: hello@goauthentik.io diff --git a/web/src/common/constants.ts b/web/src/common/constants.ts index a0c4be949..8087d0f29 100644 --- a/web/src/common/constants.ts +++ b/web/src/common/constants.ts @@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; export const ERROR_CLASS = "pf-m-danger"; export const PROGRESS_CLASS = "pf-m-in-progress"; export const CURRENT_CLASS = "pf-m-current"; -export const VERSION = "2023.6.0"; +export const VERSION = "2023.6.1"; export const TITLE_DEFAULT = "authentik"; export const ROUTE_SEPARATOR = ";";