From ef913abc7addce5c92c03b6e32572b9ced9624f4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 23 May 2020 22:01:38 +0200 Subject: [PATCH] sources/ldap: add option to disable user sync, move connection init to model --- passbook/policies/engine.py | 10 ++++- passbook/policies/process.py | 1 - passbook/policies/signals.py | 5 ++- passbook/sources/ldap/api.py | 1 + passbook/sources/ldap/connector.py | 41 ++++++++----------- passbook/sources/ldap/forms.py | 1 + .../migrations/0002_ldapsource_sync_users.py | 18 ++++++++ passbook/sources/ldap/models.py | 24 +++++++++++ passbook/sources/ldap/tasks.py | 3 -- swagger.yaml | 7 ++-- 10 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 passbook/sources/ldap/migrations/0002_ldapsource_sync_users.py diff --git a/passbook/policies/engine.py b/passbook/policies/engine.py index 34dc6d54d..5db4f8cfc 100644 --- a/passbook/policies/engine.py +++ b/passbook/policies/engine.py @@ -76,7 +76,11 @@ class PolicyEngine: key = cache_key(binding, self.request) cached_policy = cache.get(key, None) if cached_policy and self.use_cache: - LOGGER.debug("P_ENG: Taking result from cache", policy=binding.policy, cache_key=key) + LOGGER.debug( + "P_ENG: Taking result from cache", + policy=binding.policy, + cache_key=key, + ) self.__cached_policies.append(cached_policy) continue LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy) @@ -103,7 +107,9 @@ class PolicyEngine: x.result for x in self.__processes if x.result ] for result in process_results + self.__cached_policies: - LOGGER.debug("P_ENG: result", passing=result.passing, messages=result.messages) + LOGGER.debug( + "P_ENG: result", passing=result.passing, messages=result.messages + ) if result.messages: messages += result.messages if not result.passing: diff --git a/passbook/policies/process.py b/passbook/policies/process.py index 3d5ce2525..a187627a6 100644 --- a/passbook/policies/process.py +++ b/passbook/policies/process.py @@ -6,7 +6,6 @@ from typing import Optional from django.core.cache import cache from structlog import get_logger -from passbook.core.models import User from passbook.policies.exceptions import PolicyException from passbook.policies.models import PolicyBinding from passbook.policies.types import PolicyRequest, PolicyResult diff --git a/passbook/policies/signals.py b/passbook/policies/signals.py index 0f1b1a6d6..82e0b3d94 100644 --- a/passbook/policies/signals.py +++ b/passbook/policies/signals.py @@ -12,13 +12,14 @@ LOGGER = get_logger() def invalidate_policy_cache(sender, instance, **_): """Invalidate Policy cache when policy is updated""" from passbook.policies.models import Policy, PolicyBinding - from passbook.policies.process import cache_key if isinstance(instance, Policy): LOGGER.debug("Invalidating policy cache", policy=instance) total = 0 for binding in PolicyBinding.objects.filter(policy=instance): - prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}" + "*" + prefix = ( + f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}*" + ) keys = cache.keys(prefix) total += len(keys) cache.delete_many(keys) diff --git a/passbook/sources/ldap/api.py b/passbook/sources/ldap/api.py index a51a5ce12..e5ad2677c 100644 --- a/passbook/sources/ldap/api.py +++ b/passbook/sources/ldap/api.py @@ -23,6 +23,7 @@ class LDAPSourceSerializer(ModelSerializer): "group_object_filter", "user_group_membership_field", "object_uniqueness_field", + "sync_users", "sync_groups", "sync_parent_group", "property_mappings", diff --git a/passbook/sources/ldap/connector.py b/passbook/sources/ldap/connector.py index 064a6a628..748c25e9e 100644 --- a/passbook/sources/ldap/connector.py +++ b/passbook/sources/ldap/connector.py @@ -16,26 +16,10 @@ LOGGER = get_logger() class Connector: """Wrapper for ldap3 to easily manage user authentication and creation""" - _server: ldap3.Server - _connection = ldap3.Connection _source: LDAPSource def __init__(self, source: LDAPSource): self._source = source - self._server = ldap3.Server(source.server_uri) # Implement URI parsing - - def bind(self): - """Bind using Source's Credentials""" - self._connection = ldap3.Connection( - self._server, - raise_exceptions=True, - user=self._source.bind_cn, - password=self._source.bind_password, - ) - - self._connection.bind() - if self._source.start_tls: - self._connection.start_tls() @staticmethod def encode_pass(password: str) -> bytes: @@ -45,19 +29,23 @@ class Connector: @property def base_dn_users(self) -> str: """Shortcut to get full base_dn for user lookups""" - return ",".join([self._source.additional_user_dn, self._source.base_dn]) + if self._source.additional_user_dn: + return f"{self._source.additional_user_dn},{self._source.base_dn}" + return self._source.base_dn @property def base_dn_groups(self) -> str: """Shortcut to get full base_dn for group lookups""" - return ",".join([self._source.additional_group_dn, self._source.base_dn]) + if self._source.additional_group_dn: + return f"{self._source.additional_group_dn},{self._source.base_dn}" + return self._source.base_dn def sync_groups(self): """Iterate over all LDAP Groups and create passbook_core.Group instances""" if not self._source.sync_groups: - LOGGER.debug("Group syncing is disabled for this Source") + LOGGER.warning("Group syncing is disabled for this Source") return - groups = self._connection.extend.standard.paged_search( + groups = self._source.connection.extend.standard.paged_search( search_base=self.base_dn_groups, search_filter=self._source.group_object_filter, search_scope=ldap3.SUBTREE, @@ -87,7 +75,10 @@ class Connector: def sync_users(self): """Iterate over all LDAP Users and create passbook_core.User instances""" - users = self._connection.extend.standard.paged_search( + if not self._source.sync_users: + LOGGER.warning("User syncing is disabled for this Source") + return + users = self._source.connection.extend.standard.paged_search( search_base=self.base_dn_users, search_filter=self._source.user_object_filter, search_scope=ldap3.SUBTREE, @@ -101,9 +92,9 @@ class Connector: LOGGER.warning("Cannot find uniqueness Field in attributes") continue try: + defaults = self._build_object_properties(attributes) user, created = User.objects.update_or_create( - attributes__ldap_uniq=uniq, - defaults=self._build_object_properties(attributes), + attributes__ldap_uniq=uniq, defaults=defaults, ) except IntegrityError as exc: LOGGER.warning("Failed to create user", exc=exc) @@ -123,7 +114,7 @@ class Connector: def sync_membership(self): """Iterate over all Users and assign Groups using memberOf Field""" - users = self._connection.extend.standard.paged_search( + users = self._source.connection.extend.standard.paged_search( search_base=self.base_dn_users, search_filter=self._source.user_object_filter, search_scope=ldap3.SUBTREE, @@ -220,7 +211,7 @@ class Connector: LOGGER.debug("Attempting Binding as user", user=user) try: temp_connection = ldap3.Connection( - self._server, + self._source.connection.server, user=user.attributes.get("distinguishedName"), password=password, raise_exceptions=True, diff --git a/passbook/sources/ldap/forms.py b/passbook/sources/ldap/forms.py index 249ebd5af..48d71d48a 100644 --- a/passbook/sources/ldap/forms.py +++ b/passbook/sources/ldap/forms.py @@ -26,6 +26,7 @@ class LDAPSourceForm(forms.ModelForm): "group_object_filter", "user_group_membership_field", "object_uniqueness_field", + "sync_users", "sync_groups", "sync_parent_group", "property_mappings", diff --git a/passbook/sources/ldap/migrations/0002_ldapsource_sync_users.py b/passbook/sources/ldap/migrations/0002_ldapsource_sync_users.py new file mode 100644 index 000000000..27a0da2b3 --- /dev/null +++ b/passbook/sources/ldap/migrations/0002_ldapsource_sync_users.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-05-23 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_sources_ldap", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="ldapsource", + name="sync_users", + field=models.BooleanField(default=True), + ), + ] diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index 393cccfa2..34fa96e56 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -1,8 +1,10 @@ """passbook LDAP Models""" +from typing import Optional from django.core.validators import URLValidator from django.db import models from django.utils.translation import gettext_lazy as _ +from ldap3 import Connection, Server from passbook.core.models import Group, PropertyMapping, Source @@ -22,10 +24,12 @@ class LDAPSource(Source): additional_user_dn = models.TextField( help_text=_("Prepended to Base DN for User-queries."), verbose_name=_("Addition User DN"), + blank=True, ) additional_group_dn = models.TextField( help_text=_("Prepended to Base DN for Group-queries."), verbose_name=_("Addition Group DN"), + blank=True, ) user_object_filter = models.TextField( @@ -43,6 +47,7 @@ class LDAPSource(Source): default="objectSid", help_text=_("Field which contains a unique Identifier.") ) + sync_users = models.BooleanField(default=True) sync_groups = models.BooleanField(default=True) sync_parent_group = models.ForeignKey( Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT @@ -50,6 +55,25 @@ class LDAPSource(Source): form = "passbook.sources.ldap.forms.LDAPSourceForm" + _connection: Optional[Connection] + + @property + def connection(self) -> Connection: + """Get a fully connected and bound LDAP Connection""" + if not self._connection: + server = Server(self.server_uri) + self._connection = Connection( + server, + raise_exceptions=True, + user=self.bind_cn, + password=self.bind_password, + ) + + self._connection.bind() + if self.start_tls: + self._connection.start_tls() + return self._connection + class Meta: verbose_name = _("LDAP Source") diff --git a/passbook/sources/ldap/tasks.py b/passbook/sources/ldap/tasks.py index 581d27c7a..eeb1cb282 100644 --- a/passbook/sources/ldap/tasks.py +++ b/passbook/sources/ldap/tasks.py @@ -9,7 +9,6 @@ def sync_groups(source_pk: int): """Sync LDAP Groups on background worker""" source = LDAPSource.objects.get(pk=source_pk) connector = Connector(source) - connector.bind() connector.sync_groups() @@ -18,7 +17,6 @@ def sync_users(source_pk: int): """Sync LDAP Users on background worker""" source = LDAPSource.objects.get(pk=source_pk) connector = Connector(source) - connector.bind() connector.sync_users() @@ -27,7 +25,6 @@ def sync(): """Sync all sources""" for source in LDAPSource.objects.filter(enabled=True): connector = Connector(source) - connector.bind() connector.sync_users() connector.sync_groups() connector.sync_membership() diff --git a/swagger.yaml b/swagger.yaml index ac38448f0..31c87f5be 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5606,8 +5606,6 @@ definitions: - bind_cn - bind_password - base_dn - - additional_user_dn - - additional_group_dn type: object properties: pk: @@ -5654,12 +5652,10 @@ definitions: title: Addition User DN description: Prepended to Base DN for User-queries. type: string - minLength: 1 additional_group_dn: title: Addition Group DN description: Prepended to Base DN for Group-queries. type: string - minLength: 1 user_object_filter: title: User object filter description: Consider Objects matching this filter to be Users. @@ -5680,6 +5676,9 @@ definitions: description: Field which contains a unique Identifier. type: string minLength: 1 + sync_users: + title: Sync users + type: boolean sync_groups: title: Sync groups type: boolean