sources/ldap: add option to disable user sync, move connection init to model

This commit is contained in:
Jens Langhammer 2020-05-23 22:01:38 +02:00
parent 55fc5a6068
commit ef913abc7a
10 changed files with 74 additions and 37 deletions

View File

@ -76,7 +76,11 @@ class PolicyEngine:
key = cache_key(binding, self.request) key = cache_key(binding, self.request)
cached_policy = cache.get(key, None) cached_policy = cache.get(key, None)
if cached_policy and self.use_cache: 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) self.__cached_policies.append(cached_policy)
continue continue
LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy) 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 x.result for x in self.__processes if x.result
] ]
for result in process_results + self.__cached_policies: 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: if result.messages:
messages += result.messages messages += result.messages
if not result.passing: if not result.passing:

View File

@ -6,7 +6,6 @@ from typing import Optional
from django.core.cache import cache from django.core.cache import cache
from structlog import get_logger from structlog import get_logger
from passbook.core.models import User
from passbook.policies.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policies.models import PolicyBinding from passbook.policies.models import PolicyBinding
from passbook.policies.types import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult

View File

@ -12,13 +12,14 @@ LOGGER = get_logger()
def invalidate_policy_cache(sender, instance, **_): def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated""" """Invalidate Policy cache when policy is updated"""
from passbook.policies.models import Policy, PolicyBinding from passbook.policies.models import Policy, PolicyBinding
from passbook.policies.process import cache_key
if isinstance(instance, Policy): if isinstance(instance, Policy):
LOGGER.debug("Invalidating policy cache", policy=instance) LOGGER.debug("Invalidating policy cache", policy=instance)
total = 0 total = 0
for binding in PolicyBinding.objects.filter(policy=instance): 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) keys = cache.keys(prefix)
total += len(keys) total += len(keys)
cache.delete_many(keys) cache.delete_many(keys)

View File

@ -23,6 +23,7 @@ class LDAPSourceSerializer(ModelSerializer):
"group_object_filter", "group_object_filter",
"user_group_membership_field", "user_group_membership_field",
"object_uniqueness_field", "object_uniqueness_field",
"sync_users",
"sync_groups", "sync_groups",
"sync_parent_group", "sync_parent_group",
"property_mappings", "property_mappings",

View File

@ -16,26 +16,10 @@ LOGGER = get_logger()
class Connector: class Connector:
"""Wrapper for ldap3 to easily manage user authentication and creation""" """Wrapper for ldap3 to easily manage user authentication and creation"""
_server: ldap3.Server
_connection = ldap3.Connection
_source: LDAPSource _source: LDAPSource
def __init__(self, source: LDAPSource): def __init__(self, source: LDAPSource):
self._source = source 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 @staticmethod
def encode_pass(password: str) -> bytes: def encode_pass(password: str) -> bytes:
@ -45,19 +29,23 @@ class Connector:
@property @property
def base_dn_users(self) -> str: def base_dn_users(self) -> str:
"""Shortcut to get full base_dn for user lookups""" """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 @property
def base_dn_groups(self) -> str: def base_dn_groups(self) -> str:
"""Shortcut to get full base_dn for group lookups""" """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): def sync_groups(self):
"""Iterate over all LDAP Groups and create passbook_core.Group instances""" """Iterate over all LDAP Groups and create passbook_core.Group instances"""
if not self._source.sync_groups: 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 return
groups = self._connection.extend.standard.paged_search( groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups, search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter, search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
@ -87,7 +75,10 @@ class Connector:
def sync_users(self): def sync_users(self):
"""Iterate over all LDAP Users and create passbook_core.User instances""" """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_base=self.base_dn_users,
search_filter=self._source.user_object_filter, search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
@ -101,9 +92,9 @@ class Connector:
LOGGER.warning("Cannot find uniqueness Field in attributes") LOGGER.warning("Cannot find uniqueness Field in attributes")
continue continue
try: try:
defaults = self._build_object_properties(attributes)
user, created = User.objects.update_or_create( user, created = User.objects.update_or_create(
attributes__ldap_uniq=uniq, attributes__ldap_uniq=uniq, defaults=defaults,
defaults=self._build_object_properties(attributes),
) )
except IntegrityError as exc: except IntegrityError as exc:
LOGGER.warning("Failed to create user", exc=exc) LOGGER.warning("Failed to create user", exc=exc)
@ -123,7 +114,7 @@ class Connector:
def sync_membership(self): def sync_membership(self):
"""Iterate over all Users and assign Groups using memberOf Field""" """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_base=self.base_dn_users,
search_filter=self._source.user_object_filter, search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
@ -220,7 +211,7 @@ class Connector:
LOGGER.debug("Attempting Binding as user", user=user) LOGGER.debug("Attempting Binding as user", user=user)
try: try:
temp_connection = ldap3.Connection( temp_connection = ldap3.Connection(
self._server, self._source.connection.server,
user=user.attributes.get("distinguishedName"), user=user.attributes.get("distinguishedName"),
password=password, password=password,
raise_exceptions=True, raise_exceptions=True,

View File

@ -26,6 +26,7 @@ class LDAPSourceForm(forms.ModelForm):
"group_object_filter", "group_object_filter",
"user_group_membership_field", "user_group_membership_field",
"object_uniqueness_field", "object_uniqueness_field",
"sync_users",
"sync_groups", "sync_groups",
"sync_parent_group", "sync_parent_group",
"property_mappings", "property_mappings",

View File

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

View File

@ -1,8 +1,10 @@
"""passbook LDAP Models""" """passbook LDAP Models"""
from typing import Optional
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ldap3 import Connection, Server
from passbook.core.models import Group, PropertyMapping, Source from passbook.core.models import Group, PropertyMapping, Source
@ -22,10 +24,12 @@ class LDAPSource(Source):
additional_user_dn = models.TextField( additional_user_dn = models.TextField(
help_text=_("Prepended to Base DN for User-queries."), help_text=_("Prepended to Base DN for User-queries."),
verbose_name=_("Addition User DN"), verbose_name=_("Addition User DN"),
blank=True,
) )
additional_group_dn = models.TextField( additional_group_dn = models.TextField(
help_text=_("Prepended to Base DN for Group-queries."), help_text=_("Prepended to Base DN for Group-queries."),
verbose_name=_("Addition Group DN"), verbose_name=_("Addition Group DN"),
blank=True,
) )
user_object_filter = models.TextField( user_object_filter = models.TextField(
@ -43,6 +47,7 @@ class LDAPSource(Source):
default="objectSid", help_text=_("Field which contains a unique Identifier.") default="objectSid", help_text=_("Field which contains a unique Identifier.")
) )
sync_users = models.BooleanField(default=True)
sync_groups = models.BooleanField(default=True) sync_groups = models.BooleanField(default=True)
sync_parent_group = models.ForeignKey( sync_parent_group = models.ForeignKey(
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT 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" 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: class Meta:
verbose_name = _("LDAP Source") verbose_name = _("LDAP Source")

View File

@ -9,7 +9,6 @@ def sync_groups(source_pk: int):
"""Sync LDAP Groups on background worker""" """Sync LDAP Groups on background worker"""
source = LDAPSource.objects.get(pk=source_pk) source = LDAPSource.objects.get(pk=source_pk)
connector = Connector(source) connector = Connector(source)
connector.bind()
connector.sync_groups() connector.sync_groups()
@ -18,7 +17,6 @@ def sync_users(source_pk: int):
"""Sync LDAP Users on background worker""" """Sync LDAP Users on background worker"""
source = LDAPSource.objects.get(pk=source_pk) source = LDAPSource.objects.get(pk=source_pk)
connector = Connector(source) connector = Connector(source)
connector.bind()
connector.sync_users() connector.sync_users()
@ -27,7 +25,6 @@ def sync():
"""Sync all sources""" """Sync all sources"""
for source in LDAPSource.objects.filter(enabled=True): for source in LDAPSource.objects.filter(enabled=True):
connector = Connector(source) connector = Connector(source)
connector.bind()
connector.sync_users() connector.sync_users()
connector.sync_groups() connector.sync_groups()
connector.sync_membership() connector.sync_membership()

View File

@ -5606,8 +5606,6 @@ definitions:
- bind_cn - bind_cn
- bind_password - bind_password
- base_dn - base_dn
- additional_user_dn
- additional_group_dn
type: object type: object
properties: properties:
pk: pk:
@ -5654,12 +5652,10 @@ definitions:
title: Addition User DN title: Addition User DN
description: Prepended to Base DN for User-queries. description: Prepended to Base DN for User-queries.
type: string type: string
minLength: 1
additional_group_dn: additional_group_dn:
title: Addition Group DN title: Addition Group DN
description: Prepended to Base DN for Group-queries. description: Prepended to Base DN for Group-queries.
type: string type: string
minLength: 1
user_object_filter: user_object_filter:
title: User object filter title: User object filter
description: Consider Objects matching this filter to be Users. description: Consider Objects matching this filter to be Users.
@ -5680,6 +5676,9 @@ definitions:
description: Field which contains a unique Identifier. description: Field which contains a unique Identifier.
type: string type: string
minLength: 1 minLength: 1
sync_users:
title: Sync users
type: boolean
sync_groups: sync_groups:
title: Sync groups title: Sync groups
type: boolean type: boolean