sources/ldap: add e2e LDAP source tests (#4462)

* start adding more LDAP source tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve healthcheck

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* try local webdriver

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add full samba tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix locale types

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-01-19 15:03:56 +01:00 committed by GitHub
parent 8709f3300c
commit c61529e4d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 10 deletions

View File

@ -124,7 +124,7 @@ jobs:
- name: saml - name: saml
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap - name: ldap
glob: tests/e2e/test_provider_ldap* glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: flows - name: flows
glob: tests/e2e/test_flows* glob: tests/e2e/test_flows*
steps: steps:

View File

@ -448,7 +448,7 @@ class NotificationTransport(SerializerModel):
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false
return send_mail(mail.__dict__) # pylint: disable=no-value-for-parameter return send_mail(mail.__dict__) # pylint: disable=no-value-for-parameter
except (SMTPException, ConnectionError, OSError) as exc: except (SMTPException, ConnectionError, OSError) as exc:
raise NotificationTransportError from exc raise NotificationTransportError(exc) from exc
@property @property
def serializer(self) -> "Serializer": def serializer(self) -> "Serializer":

View File

@ -38,7 +38,6 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
try: try:
defaults = self.build_group_properties(group_dn, **attributes) defaults = self.build_group_properties(group_dn, **attributes)
defaults["parent"] = self._source.sync_parent_group defaults["parent"] = self._source.sync_parent_group
self._logger.debug("Creating group with attributes", **defaults)
if "name" not in defaults: if "name" not in defaults:
raise IntegrityError("Name was not set by propertymappings") raise IntegrityError("Name was not set by propertymappings")
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd # Special check for `users` field, as this is an M2M relation, and cannot be sync'd
@ -51,6 +50,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
}, },
defaults, defaults,
) )
self._logger.debug("Created group with attributes", **defaults)
except (IntegrityError, FieldError, TypeError, AttributeError) as exc: except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,

View File

@ -33,11 +33,10 @@ class LDAPSyncTests(TestCase):
"""Test Cached auth""" """Test Cached auth"""
self.source.property_mappings.set( self.source.property_mappings.set(
LDAPPropertyMapping.objects.filter( LDAPPropertyMapping.objects.filter(
Q(name__startswith="authentik default LDAP Mapping") Q(managed__startswith="goauthentik.io/sources/ldap/default-")
| Q(name__startswith="authentik default Active Directory Mapping") | Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
) )
) )
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD)) connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source) user_sync = UserLDAPSynchronizer(self.source)

View File

@ -0,0 +1,165 @@
"""test LDAP Source"""
from typing import Any, Optional
from django.db.models import Q
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Group, User
from authentik.lib.generators import generate_id, generate_key
from authentik.sources.ldap.auth import LDAPBackend
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from tests.e2e.utils import SeleniumTestCase, retry
class TestSourceLDAPSamba(SeleniumTestCase):
"""test LDAP Source"""
def setUp(self):
self.admin_password = generate_key()
super().setUp()
def get_container_specs(self) -> Optional[dict[str, Any]]:
return {
"image": "ghcr.io/beryju/test-samba-dc:latest",
"detach": True,
"cap_add": ["SYS_ADMIN"],
"ports": {
"389": "389/tcp",
},
"auto_remove": True,
"environment": {
"SMB_DOMAIN": "test.goauthentik.io",
"SMB_NETBIOS": "goauthentik",
"SMB_ADMIN_PASSWORD": self.admin_password,
},
}
@retry()
@apply_blueprint(
"system/sources-ldap.yaml",
)
def test_source_sync(self):
"""Test Sync"""
source = LDAPSource.objects.create(
name=generate_id(),
slug=generate_id(),
server_uri="ldap://localhost",
bind_cn="administrator@test.goauthentik.io",
bind_password=self.admin_password,
base_dn="dc=test,dc=goauthentik,dc=io",
additional_user_dn="ou=users",
additional_group_dn="ou=groups",
)
source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
)
)
source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
)
UserLDAPSynchronizer(source).sync()
self.assertTrue(User.objects.filter(username="bob").exists())
self.assertTrue(User.objects.filter(username="james").exists())
self.assertTrue(User.objects.filter(username="john").exists())
self.assertTrue(User.objects.filter(username="harry").exists())
@retry()
@apply_blueprint(
"system/sources-ldap.yaml",
)
def test_source_sync_group(self):
"""Test Sync"""
source = LDAPSource.objects.create(
name=generate_id(),
slug=generate_id(),
server_uri="ldap://localhost",
bind_cn="administrator@test.goauthentik.io",
bind_password=self.admin_password,
base_dn="dc=test,dc=goauthentik,dc=io",
additional_user_dn="ou=users",
additional_group_dn="ou=groups",
)
source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
)
)
source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
)
GroupLDAPSynchronizer(source).sync()
UserLDAPSynchronizer(source).sync()
MembershipLDAPSynchronizer(source).sync()
self.assertIsNotNone(User.objects.get(username="bob"))
self.assertIsNotNone(User.objects.get(username="james"))
self.assertIsNotNone(User.objects.get(username="john"))
self.assertIsNotNone(User.objects.get(username="harry"))
self.assertIsNotNone(Group.objects.get(name="dev"))
self.assertEqual(
list(User.objects.get(username="bob").ak_groups.all()), [Group.objects.get(name="dev")]
)
self.assertEqual(list(User.objects.get(username="james").ak_groups.all()), [])
self.assertEqual(
list(User.objects.get(username="john").ak_groups.all().order_by("name")),
[Group.objects.get(name="admins"), Group.objects.get(name="dev")],
)
self.assertEqual(list(User.objects.get(username="harry").ak_groups.all()), [])
@retry()
@apply_blueprint(
"system/sources-ldap.yaml",
)
def test_sync_password(self):
"""Test Sync"""
source = LDAPSource.objects.create(
name=generate_id(),
slug=generate_id(),
server_uri="ldap://localhost",
bind_cn="administrator@test.goauthentik.io",
bind_password=self.admin_password,
base_dn="dc=test,dc=goauthentik,dc=io",
additional_user_dn="ou=users",
additional_group_dn="ou=groups",
)
source.property_mappings.set(
LDAPPropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
)
)
source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(name="goauthentik.io/sources/ldap/default-name")
)
UserLDAPSynchronizer(source).sync()
username = "bob"
password = generate_id()
result = self.container.exec_run(
["samba-tool", "user", "setpassword", username, "--newpassword", password]
)
self.assertEqual(result.exit_code, 0)
user: User = User.objects.get(username=username)
# Ensure user has an unusable password directly after sync
self.assertFalse(user.has_usable_password())
# Auth (which will fallback to bind)
LDAPBackend().auth_user(source, password, username=username)
user.refresh_from_db()
# User should now have a usable password in the database
self.assertTrue(user.has_usable_password())
self.assertTrue(user.check_password(password))
# Set new password
new_password = generate_id()
result = self.container.exec_run(
["samba-tool", "user", "setpassword", username, "--newpassword", new_password]
)
self.assertEqual(result.exit_code, 0)
# Sync again
UserLDAPSynchronizer(source).sync()
user.refresh_from_db()
# Since password in samba was checked, it should be invalidated here too
self.assertFalse(user.has_usable_password())

View File

@ -54,7 +54,6 @@ class SeleniumTestCase(StaticLiveServerTestCase):
self.maxDiff = None self.maxDiff = None
self.wait_timeout = 60 self.wait_timeout = 60
self.driver = self._get_driver() self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(30) self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, self.wait_timeout) self.wait = WebDriverWait(self.driver, self.wait_timeout)
self.logger = get_logger() self.logger = get_logger()
@ -77,7 +76,9 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def _start_container(self, specs: dict[str, Any]) -> Container: def _start_container(self, specs: dict[str, Any]) -> Container:
client: DockerClient = from_env() client: DockerClient = from_env()
container = client.containers.run(**specs) container = client.containers.run(**specs)
if "healthcheck" not in specs: container.reload()
state = container.attrs.get("State", {})
if "Health" not in state:
return container return container
while True: while True:
container.reload() container.reload()
@ -100,12 +101,18 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def _get_driver(self) -> WebDriver: def _get_driver(self) -> WebDriver:
count = 0 count = 0
try:
return webdriver.Chrome()
except WebDriverException:
pass
while count < RETRIES: while count < RETRIES:
try: try:
return webdriver.Remote( driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub", command_executor="http://localhost:4444/wd/hub",
options=webdriver.ChromeOptions(), options=webdriver.ChromeOptions(),
) )
driver.maximize_window()
return driver
except WebDriverException: except WebDriverException:
count += 1 count += 1
raise ValueError(f"Webdriver failed after {RETRIES}.") raise ValueError(f"Webdriver failed after {RETRIES}.")

View File

@ -1,5 +1,6 @@
import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants"; import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { PluralCategory } from "make-plural";
import { Messages, i18n } from "@lingui/core"; import { Messages, i18n } from "@lingui/core";
import { detect, fromNavigator, fromUrl } from "@lingui/detect-locale"; import { detect, fromNavigator, fromUrl } from "@lingui/detect-locale";
@ -7,7 +8,7 @@ import { t } from "@lingui/macro";
interface Locale { interface Locale {
locale: Messages; locale: Messages;
plurals: (n: string | number, ord?: boolean | undefined) => string; plurals: (n: string | number, ord?: boolean | undefined) => PluralCategory;
} }
export const LOCALES: { export const LOCALES: {