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:
parent
8709f3300c
commit
c61529e4d4
|
@ -124,7 +124,7 @@ jobs:
|
|||
- name: saml
|
||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||
- name: ldap
|
||||
glob: tests/e2e/test_provider_ldap*
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
steps:
|
||||
|
|
|
@ -448,7 +448,7 @@ class NotificationTransport(SerializerModel):
|
|||
# pyright: reportGeneralTypeIssues=false
|
||||
return send_mail(mail.__dict__) # pylint: disable=no-value-for-parameter
|
||||
except (SMTPException, ConnectionError, OSError) as exc:
|
||||
raise NotificationTransportError from exc
|
||||
raise NotificationTransportError(exc) from exc
|
||||
|
||||
@property
|
||||
def serializer(self) -> "Serializer":
|
||||
|
|
|
@ -38,7 +38,6 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||
try:
|
||||
defaults = self.build_group_properties(group_dn, **attributes)
|
||||
defaults["parent"] = self._source.sync_parent_group
|
||||
self._logger.debug("Creating group with attributes", **defaults)
|
||||
if "name" not in defaults:
|
||||
raise IntegrityError("Name was not set by propertymappings")
|
||||
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd
|
||||
|
@ -51,6 +50,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||
},
|
||||
defaults,
|
||||
)
|
||||
self._logger.debug("Created group with attributes", **defaults)
|
||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
|
|
|
@ -33,11 +33,10 @@ class LDAPSyncTests(TestCase):
|
|||
"""Test Cached auth"""
|
||||
self.source.property_mappings.set(
|
||||
LDAPPropertyMapping.objects.filter(
|
||||
Q(name__startswith="authentik default LDAP Mapping")
|
||||
| Q(name__startswith="authentik default Active Directory Mapping")
|
||||
Q(managed__startswith="goauthentik.io/sources/ldap/default-")
|
||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
|
||||
)
|
||||
)
|
||||
self.source.save()
|
||||
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source)
|
||||
|
|
|
@ -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())
|
|
@ -54,7 +54,6 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||
self.maxDiff = None
|
||||
self.wait_timeout = 60
|
||||
self.driver = self._get_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, self.wait_timeout)
|
||||
self.logger = get_logger()
|
||||
|
@ -77,7 +76,9 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||
def _start_container(self, specs: dict[str, Any]) -> Container:
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(**specs)
|
||||
if "healthcheck" not in specs:
|
||||
container.reload()
|
||||
state = container.attrs.get("State", {})
|
||||
if "Health" not in state:
|
||||
return container
|
||||
while True:
|
||||
container.reload()
|
||||
|
@ -100,12 +101,18 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
count = 0
|
||||
try:
|
||||
return webdriver.Chrome()
|
||||
except WebDriverException:
|
||||
pass
|
||||
while count < RETRIES:
|
||||
try:
|
||||
return webdriver.Remote(
|
||||
driver = webdriver.Remote(
|
||||
command_executor="http://localhost:4444/wd/hub",
|
||||
options=webdriver.ChromeOptions(),
|
||||
)
|
||||
driver.maximize_window()
|
||||
return driver
|
||||
except WebDriverException:
|
||||
count += 1
|
||||
raise ValueError(f"Webdriver failed after {RETRIES}.")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { PluralCategory } from "make-plural";
|
||||
|
||||
import { Messages, i18n } from "@lingui/core";
|
||||
import { detect, fromNavigator, fromUrl } from "@lingui/detect-locale";
|
||||
|
@ -7,7 +8,7 @@ import { t } from "@lingui/macro";
|
|||
|
||||
interface Locale {
|
||||
locale: Messages;
|
||||
plurals: (n: string | number, ord?: boolean | undefined) => string;
|
||||
plurals: (n: string | number, ord?: boolean | undefined) => PluralCategory;
|
||||
}
|
||||
|
||||
export const LOCALES: {
|
||||
|
|
Reference in New Issue