sources/ldap: add option to disable user sync, move connection init to model
This commit is contained in:
parent
55fc5a6068
commit
ef913abc7a
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in New Issue