diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 649299d3..2dd2279e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,15 +13,6 @@ jobs: TOXENV: docs steps: - - name: Install LDAP libs - run: | - sudo apt-get update - # https://www.python-ldap.org/en/latest/installing.html#debian - sudo apt-get install slapd ldap-utils libldap2-dev libsasl2-dev - # https://github.com/python-ldap/python-ldap/issues/370 - sudo apt-get install apparmor-utils - sudo aa-disable /usr/sbin/slapd - - uses: actions/checkout@v2 - name: Set up Python diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 8f808632..9f0a112f 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -13,15 +13,6 @@ jobs: TOXENV: packaging steps: - - name: Install LDAP libs - run: | - sudo apt-get update - # https://www.python-ldap.org/en/latest/installing.html#debian - sudo apt-get install slapd ldap-utils libldap2-dev libsasl2-dev - # https://github.com/python-ldap/python-ldap/issues/370 - sudo apt-get install apparmor-utils - sudo aa-disable /usr/sbin/slapd - - uses: actions/checkout@v2 - name: Set up Python diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bf35e6c..3e249267 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,15 +27,6 @@ jobs: TOXENV: ${{ matrix.tox-environment }} steps: - - name: Install LDAP libs - run: | - sudo apt-get update - # https://www.python-ldap.org/en/latest/installing.html#debian - sudo apt-get install slapd ldap-utils libldap2-dev libsasl2-dev - # https://github.com/python-ldap/python-ldap/issues/370 - sudo apt-get install apparmor-utils - sudo aa-disable /usr/sbin/slapd - - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/django_auth_ldap/backend.py b/django_auth_ldap/backend.py index 96ca73a7..f95ed683 100644 --- a/django_auth_ldap/backend.py +++ b/django_auth_ldap/backend.py @@ -51,88 +51,126 @@ import re import warnings from functools import reduce +from logging import Logger +from typing import ( + AbstractSet, + Any, + Callable, + Collection, + Dict, + FrozenSet, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) import django.conf import django.dispatch -import ldap +import ldap3 from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, Permission +from django.contrib.auth.models import AbstractUser, Group, Permission from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.utils.inspect import func_supports_parameter +from django.http import HttpRequest +from ldap3 import Connection, Server +from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult +from ldap3.utils.dn import escape_rdn from django_auth_ldap.config import ( + AbstractLDAPSearch, ConfigurationWarning, LDAPGroupQuery, + LDAPGroupType, + LDAPGroupTypeIsMember, LDAPSearch, _LDAPConfig, ) -logger = _LDAPConfig.get_logger() +# TODO remove try/catch when dropping support for Django 2.2 +try: + from django.contrib.auth.backends import BaseBackend +except ImportError: + # BaseBackend was first introduced to Django in version 3.0, + # thus to support Django 2.2 the ImportError must be caught. + BaseBackend = object + +T = TypeVar("T") +logger: Logger = _LDAPConfig.get_logger() # Exported signals # Allows clients to perform custom user population. # Passed arguments: user, ldap_user -populate_user = django.dispatch.Signal() +populate_user: django.dispatch.Signal = django.dispatch.Signal() # Allows clients to inspect and perform special handling of LDAPError # exceptions. Exceptions raised by handlers will be propagated out. # Passed arguments: context, user, exception -ldap_error = django.dispatch.Signal() +ldap_error: django.dispatch.Signal = django.dispatch.Signal() -class LDAPBackend: +class LDAPBackend(BaseBackend): """ The main backend class. This implements the auth backend API, although it actually delegates most of its work to _LDAPUser, which is defined next. """ - supports_anonymous_user = False - supports_object_permissions = True - supports_inactive_user = False - - _settings = None - _ldap = None # The cached ldap module (or mock object) - - # This is prepended to our internal setting names to produce the names we - # expect in Django's settings file. Subclasses can change this in order to - # support multiple collections of settings. - settings_prefix = "AUTH_LDAP_" + def __init__(self) -> None: + self._settings: Optional[LDAPSettings] = None + self._server: Optional[Server] = None - # Default settings to override the built-in defaults. - default_settings = {} - - def __getstate__(self): + def __getstate__(self) -> dict: """ Exclude certain cached properties from pickling. """ - return { - k: v for k, v in self.__dict__.items() if k not in ["_settings", "_ldap"] - } + return {k: v for k, v in self.__dict__.items() if k != "_server"} + + def __setstate__(self, state: Dict[str, Any]): + """ + Set excluded properties from pickling. + """ + self.__dict__.update(state) + self._server = None @property - def settings(self): + def settings(self) -> "LDAPSettings": if self._settings is None: - self._settings = LDAPSettings(self.settings_prefix, self.default_settings) + self._settings = LDAPSettings() return self._settings @settings.setter - def settings(self, settings): + def settings(self, settings: "LDAPSettings") -> None: self._settings = settings - @property - def ldap(self): - if self._ldap is None: - options = getattr(django.conf.settings, "AUTH_LDAP_GLOBAL_OPTIONS", None) + def get_server(self, request: Optional[HttpRequest]) -> Server: + uri = self.settings.SERVER_URI + uri_callable = callable(uri) + + if self._server is None or uri_callable: + if uri_callable: + uri = cast(Callable, uri)(request) - self._ldap = _LDAPConfig.get_ldap(options) + _server = Server(uri, use_ssl=self.settings.START_TLS) + if _server.ssl: + logger.debug("Use SSL") - return self._ldap + if uri_callable: + self._server = None + else: + self._server = _server - def get_user_model(self): + return _server + + return self._server + + def get_user_model(self) -> Type[AbstractUser]: """ By default, this will return the model class configured by AUTH_USER_MODEL. Subclasses may wish to override it and return a proxy @@ -144,7 +182,13 @@ def get_user_model(self): # The Django auth backend API # - def authenticate(self, request, username=None, password=None, **kwargs): + def authenticate( + self, + request: Optional[HttpRequest], + username: Optional[str] = None, + password: str = "", + **kwargs: Any + ) -> Optional[AbstractUser]: if username is None: return None @@ -157,7 +201,7 @@ def authenticate(self, request, username=None, password=None, **kwargs): return user - def get_user(self, user_id): + def get_user(self, user_id: Any) -> Optional[AbstractUser]: user = None try: @@ -168,20 +212,26 @@ def get_user(self, user_id): return user - def has_perm(self, user, perm, obj=None): + def has_perm( + self, user: AbstractUser, perm: str, obj: Optional[Any] = None + ) -> bool: return perm in self.get_all_permissions(user, obj) - def has_module_perms(self, user, app_label): + def has_module_perms(self, user: AbstractUser, app_label: str) -> bool: for perm in self.get_all_permissions(user): if perm[: perm.index(".")] == app_label: return True return False - def get_all_permissions(self, user, obj=None): + def get_all_permissions( + self, user: AbstractUser, obj: Optional[Any] = None + ) -> Set[str]: return self.get_group_permissions(user, obj) - def get_group_permissions(self, user, obj=None): + def get_group_permissions( + self, user: AbstractUser, obj: Optional[Any] = None + ) -> Set[str]: if not hasattr(user, "ldap_user") and self.settings.AUTHORIZE_ALL_USERS: _LDAPUser(self, user=user) # This sets user.ldap_user @@ -196,7 +246,7 @@ def get_group_permissions(self, user, obj=None): # Bonus API: populate the Django user from LDAP without authenticating. # - def populate_user(self, username): + def populate_user(self, username: str) -> Optional[AbstractUser]: ldap_user = _LDAPUser(self, username=username) return ldap_user.populate_user() @@ -204,13 +254,17 @@ def populate_user(self, username): # Hooks for subclasses # - def authenticate_ldap_user(self, ldap_user, password): + def authenticate_ldap_user( + self, ldap_user: "_LDAPUser", password: str + ) -> Optional[AbstractUser]: """ Returns an authenticated Django user or None. """ return ldap_user.authenticate(password) - def get_or_build_user(self, username, ldap_user): + def get_or_build_user( + self, username: str, ldap_user: "_LDAPUser" + ) -> Tuple[AbstractUser, bool]: """ This must return a (User, built) 2-tuple for the given LDAP user. @@ -224,8 +278,16 @@ def get_or_build_user(self, username, ldap_user): model = self.get_user_model() if self.settings.USER_QUERY_FIELD: + if self.settings.USER_ATTR_MAP is None: + raise TypeError( + "%s should not be None" + % self.settings._prepend_prefix("USER_ATTR_MAP") + ) + query_field = self.settings.USER_QUERY_FIELD - query_value = ldap_user.attrs[self.settings.USER_ATTR_MAP[query_field]][0] + query_value = ldap_user.get_single_attr( + self.settings.USER_ATTR_MAP[query_field] + ) lookup = query_field else: query_field = model.USERNAME_FIELD @@ -240,12 +302,12 @@ def get_or_build_user(self, username, ldap_user): else: built = False - return (user, built) + return user, built - def ldap_to_django_username(self, username): + def ldap_to_django_username(self, username: str) -> str: return username - def django_to_ldap_username(self, username): + def django_to_ldap_username(self, username: str) -> str: return username @@ -265,28 +327,33 @@ class _LDAPUser: class AuthenticationFailed(Exception): pass - # Defaults - _user = None - _user_dn = None - _user_attrs = None - _groups = None - _group_permissions = None - _connection = None - _connection_bound = False - # # Initialization # - def __init__(self, backend, username=None, user=None, request=None): + def __init__( + self, + backend: LDAPBackend, + username: Optional[str] = None, + user: Optional[AbstractUser] = None, + request: Optional[HttpRequest] = None, + ) -> None: """ A new LDAPUser must be initialized with either a username or an authenticated User object. If a user is given, the username will be ignored. """ - self.backend = backend - self._username = username - self._request = request + self._user_dn: Optional[str] = None + self._user_attrs: Optional[Dict[str, Any]] = None + self._groups: Optional[_LDAPUserGroups] = None + self._group_permissions: Optional[Set[str]] = None + self._connection: Optional[Connection] = None + self._connection_bound: bool = False + + self.backend: LDAPBackend = backend + self._user: Optional[AbstractUser] = user + self._username: Optional[str] = username + self._request: Optional[HttpRequest] = request if user is not None: self._set_authenticated_user(user) @@ -294,7 +361,7 @@ def __init__(self, backend, username=None, user=None, request=None): if username is None and user is None: raise Exception("Internal error: _LDAPUser improperly initialized.") - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: dict) -> "_LDAPUser": obj = object.__new__(type(self)) obj.backend = self.backend obj._user = copy.deepcopy(self._user, memo) @@ -312,7 +379,7 @@ def __deepcopy__(self, memo): return obj - def __getstate__(self): + def __getstate__(self) -> Dict[str, Any]: """ Most of our properties are cached from the LDAP server. We only want to pickle a few crucial things. @@ -323,7 +390,20 @@ def __getstate__(self): if k in ["backend", "_username", "_user"] } - def _set_authenticated_user(self, user): + def __setstate__(self, state: Dict[str, Any]): + """ + Set excluded properties from pickling. + """ + self.__dict__.update(state) + self._user_dn = None + self._user_attrs = None + self._groups = None + self._group_permissions = None + self._connection = None + self._connection_bound = False + self._request = None + + def _set_authenticated_user(self, user: AbstractUser) -> None: self._user = user self._username = self.backend.django_to_ldap_username(user.get_username()) @@ -331,18 +411,14 @@ def _set_authenticated_user(self, user): user.ldap_username = self._username @property - def ldap(self): - return self.backend.ldap - - @property - def settings(self): + def settings(self) -> "LDAPSettings": return self.backend.settings # # Entry points # - def authenticate(self, password): + def authenticate(self, password: str) -> AbstractUser: """ Authenticates against the LDAP directory and returns the corresponding User object if successful. Returns None on failure. @@ -352,12 +428,10 @@ def authenticate(self, password): try: self._authenticate_user_dn(password) self._check_requirements() - self._get_or_create_user() - - user = self._user + user = self._get_or_create_user() except self.AuthenticationFailed as e: logger.debug("Authentication failed for {}: {}".format(self._username, e)) - except ldap.LDAPError as e: + except LDAPException as e: results = ldap_error.send( type(self.backend), context="authenticate", @@ -376,7 +450,7 @@ def authenticate(self, password): return user - def get_group_permissions(self): + def get_group_permissions(self) -> Set[str]: """ If allowed by the configuration, this returns the set of permissions defined by the user's LDAP group memberships. @@ -388,7 +462,7 @@ def get_group_permissions(self): try: if self.dn is not None: self._load_group_permissions() - except ldap.LDAPError as e: + except LDAPException as e: results = ldap_error.send( type(self.backend), context="get_group_permissions", @@ -404,7 +478,7 @@ def get_group_permissions(self): return self._group_permissions - def populate_user(self): + def populate_user(self) -> AbstractUser: """ Populates the Django user object using the default bind credentials. """ @@ -414,10 +488,11 @@ def populate_user(self): # self.attrs will only be non-None if we were able to load this user # from the LDAP directory, so this filters out nonexistent users. if self.attrs is not None: - self._get_or_create_user(force_populate=True) + user = self._get_or_create_user(force_populate=True) + else: + user = self._user - user = self._user - except ldap.LDAPError as e: + except LDAPException as e: results = ldap_error.send( type(self.backend), context="populate_user", @@ -441,29 +516,45 @@ def populate_user(self): # @property - def dn(self): + def dn(self) -> Optional[str]: if self._user_dn is None: self._load_user_dn() return self._user_dn @property - def attrs(self): + def attrs(self) -> Optional[Dict[str, Any]]: if self._user_attrs is None: self._load_user_attrs() return self._user_attrs + def get_single_attr(self, name) -> Any: + if self.attrs is None: + raise TypeError("The attribute attr should not be None") + + value = self.attrs[name] + if isinstance(value, List): + value_len = len(value) + if value_len == 1: + value = value[0] + elif value_len == 0: + raise ValueError("The attribute '%s' has no entries" % name) + else: + raise ValueError("The attribute '%s' has more than one entry" % name) + + return value + @property - def group_dns(self): + def group_dns(self) -> Set[str]: return self._get_groups().get_group_dns() @property - def group_names(self): + def group_names(self) -> Set[str]: return self._get_groups().get_group_names() @property - def connection(self): + def connection(self) -> Connection: if not self._connection_bound: self._bind() @@ -473,7 +564,7 @@ def connection(self): # Authentication # - def _authenticate_user_dn(self, password): + def _authenticate_user_dn(self, password: str) -> None: """ Binds to the LDAP server with the user's DN and password. Raises AuthenticationFailed on failure. @@ -485,28 +576,30 @@ def _authenticate_user_dn(self, password): sticky = self.settings.BIND_AS_AUTHENTICATING_USER self._bind_as(self.dn, password, sticky=sticky) - except ldap.INVALID_CREDENTIALS: + except LDAPInvalidCredentialsResult: raise self.AuthenticationFailed("user DN/password rejected by LDAP server.") - def _load_user_attrs(self): + def _load_user_attrs(self) -> None: if self.dn is not None: search = LDAPSearch( - self.dn, ldap.SCOPE_BASE, attrlist=self.settings.USER_ATTRLIST + self.dn, ldap3.BASE, attrlist=self.settings.USER_ATTRLIST ) results = search.execute(self.connection) - if results is not None and len(results) > 0: - self._user_attrs = results[0][1] + if results is not None: + result = next(iter(results), None) + if result is not None: + self._user_attrs = result[1] - def _load_user_dn(self): + def _load_user_dn(self) -> None: """ Populates self._user_dn with the distinguished name of our user. This will either construct the DN from a template in AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it. If we have to search, we'll cache the DN. - """ + if self._using_simple_bind_mode(): self._user_dn = self._construct_simple_user_dn() else: @@ -520,15 +613,21 @@ def _load_user_dn(self): else: self._user_dn = self._search_for_user_dn() - def _using_simple_bind_mode(self): + def _using_simple_bind_mode(self) -> bool: return self.settings.USER_DN_TEMPLATE is not None - def _construct_simple_user_dn(self): + def _construct_simple_user_dn(self) -> str: + if self.settings.USER_DN_TEMPLATE is None: + raise ImproperlyConfigured( + "%s should not be None" + % self.settings._prepend_prefix("USER_DN_TEMPLATE") + ) + template = self.settings.USER_DN_TEMPLATE - username = ldap.dn.escape_dn_chars(self._username) + username = escape_rdn(self._username) return template % {"user": username} - def _search_for_user_dn(self): + def _search_for_user_dn(self) -> Optional[str]: """ Searches the directory for a user matching AUTH_LDAP_USER_SEARCH. Populates self._user_dn and self._user_attrs. @@ -536,18 +635,18 @@ def _search_for_user_dn(self): search = self.settings.USER_SEARCH if search is None: raise ImproperlyConfigured( - "AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance." + "%s must be an LDAPSearch instance." + % self.settings._prepend_prefix("USER_SEARCH") ) results = search.execute(self.connection, {"user": self._username}) + user_dn = None if results is not None and len(results) == 1: (user_dn, self._user_attrs) = next(iter(results)) - else: - user_dn = None return user_dn - def _check_requirements(self): + def _check_requirements(self) -> None: """ Checks all authentication requirements beyond credentials. Raises AuthenticationFailed on failure. @@ -555,7 +654,7 @@ def _check_requirements(self): self._check_required_group() self._check_denied_group() - def _check_required_group(self): + def _check_required_group(self) -> bool: """ Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None. @@ -568,12 +667,13 @@ def _check_required_group(self): result = required_group_dn.resolve(self) if not result: raise self.AuthenticationFailed( - "user does not satisfy AUTH_LDAP_REQUIRE_GROUP" + "user does not satisfy %s" + % self.settings._prepend_prefix("REQUIRE_GROUP") ) return True - def _check_denied_group(self): + def _check_denied_group(self) -> bool: """ Returns True if the negative group requirement (AUTH_LDAP_DENY_GROUP) is met. Always returns True if AUTH_LDAP_DENY_GROUP is None. @@ -584,7 +684,8 @@ def _check_denied_group(self): is_member = self._get_groups().is_member_of(denied_group_dn) if is_member: raise self.AuthenticationFailed( - "user does not satisfy AUTH_LDAP_DENY_GROUP" + "user does not satisfy %s" + % self.settings._prepend_prefix("DENY_GROUP") ) return True @@ -593,7 +694,7 @@ def _check_denied_group(self): # User management # - def _get_or_create_user(self, force_populate=False): + def _get_or_create_user(self, force_populate: bool = False) -> AbstractUser: """ Loads the User model object from the database or creates it if it doesn't exist. Also populates the fields, subject to @@ -601,6 +702,9 @@ def _get_or_create_user(self, force_populate=False): """ save_user = False + if self._username is None: + raise TypeError("The username should not be None") + username = self.backend.ldap_to_django_username(self._username) self._user, built = self.backend.get_or_build_user(username, self) @@ -612,7 +716,8 @@ def _get_or_create_user(self, force_populate=False): if built: if self.settings.NO_NEW_USERS: raise self.AuthenticationFailed( - "user does not satisfy AUTH_LDAP_NO_NEW_USERS" + "user does not satisfy %s" + % self.settings._prepend_prefix("NO_NEW_USERS") ) logger.debug("Creating Django user {}".format(username)) @@ -632,53 +737,68 @@ def _get_or_create_user(self, force_populate=False): self._user.save() # This has to wait until we're sure the user has a pk. - if self.settings.MIRROR_GROUPS or self.settings.MIRROR_GROUPS_EXCEPT: - self._normalize_mirror_settings() - self._mirror_groups() + mg, mge = self._normalize_mirror_settings() + if (mg is not None and mg is not False) or mge is not None: + mg = None if isinstance(mg, bool) else mg + self._mirror_groups(mg, mge) + + return self._user - def _populate_user(self): + def _populate_user(self) -> None: """ Populates our User object with information from the LDAP directory. """ self._populate_user_from_attributes() self._populate_user_from_group_memberships() - def _populate_user_from_attributes(self): - for field, attr in self.settings.USER_ATTR_MAP.items(): - try: - value = self.attrs[attr][0] - except (TypeError, LookupError): - # TypeError occurs when self.attrs is None as we were unable to - # load this user's attributes. - logger.warning( - "{} does not have a value for the attribute {}".format( - self.dn, attr + def _populate_user_from_attributes(self) -> None: + if self.settings.USER_ATTR_MAP: + for field, attr in self.settings.USER_ATTR_MAP.items(): + try: + value = self.get_single_attr(attr) + except (TypeError, LookupError): + # TypeError occurs when self.attrs is None as we were unable to + # load this user's attributes. + logger.warning( + "{} does not have a value for the attribute {}".format( + self.dn, attr + ) ) - ) - else: - setattr(self._user, field, value) + else: + setattr(self._user, field, value) - def _populate_user_from_group_memberships(self): - for field, group_dns in self.settings.USER_FLAGS_BY_GROUP.items(): - try: - query = self._normalize_group_dns(group_dns) - except ValueError as e: - raise ImproperlyConfigured( - "{}: {}", self.settings._name("USER_FLAGS_BY_GROUP"), e - ) + def _populate_user_from_group_memberships(self) -> None: + if self.settings.USER_FLAGS_BY_GROUP: + for field, group_dns in self.settings.USER_FLAGS_BY_GROUP.items(): + try: + query = self._normalize_group_dns(group_dns) + except ValueError as e: + raise ImproperlyConfigured( + "{}: {}", + self.settings._prepend_prefix("USER_FLAGS_BY_GROUP"), + e, + ) - value = query.resolve(self) - setattr(self._user, field, value) + value = query.resolve(self) + setattr(self._user, field, value) - def _normalize_group_dns(self, group_dns): + def _normalize_group_dns( + self, + group_dns: Union[ + str, + LDAPGroupQuery, + List[Union[str, LDAPGroupQuery]], + Tuple[Union[str, LDAPGroupQuery]], + ], + ) -> LDAPGroupQuery: """ Converts one or more group DNs to an LDAPGroupQuery. group_dns may be a string, a non-empty list or tuple of strings, or an LDAPGroupQuery. The result will be an LDAPGroupQuery. A list or tuple will be joined with the | operator. - """ + if isinstance(group_dns, LDAPGroupQuery): query = group_dns elif isinstance(group_dns, str): @@ -690,34 +810,36 @@ def _normalize_group_dns(self, group_dns): return query - def _normalize_mirror_settings(self): + def _normalize_mirror_settings( + self, + ) -> Union[ + Tuple[Union[bool, FrozenSet[str]], None], + Tuple[None, FrozenSet[str]], + Tuple[None, None], + ]: """ Validates the group mirroring settings and converts them as necessary. """ - def malformed_mirror_groups_except(): + def malformed_mirror_groups_except() -> ImproperlyConfigured: return ImproperlyConfigured( "{} must be a collection of group names".format( - self.settings._name("MIRROR_GROUPS_EXCEPT") + self.settings._prepend_prefix("MIRROR_GROUPS_EXCEPT") ) ) - def malformed_mirror_groups(): + def malformed_mirror_groups() -> ImproperlyConfigured: return ImproperlyConfigured( - "{} must be True or a collection of group names".format( - self.settings._name("MIRROR_GROUPS") + "{} must be a bool or a collection of group names".format( + self.settings._prepend_prefix("MIRROR_GROUPS") ) ) - mge = self.settings.MIRROR_GROUPS_EXCEPT mg = self.settings.MIRROR_GROUPS + mge = self.settings.MIRROR_GROUPS_EXCEPT if mge is not None: - if isinstance(mge, (set, frozenset)): - pass - elif isinstance(mge, (list, tuple)): - mge = self.settings.MIRROR_GROUPS_EXCEPT = frozenset(mge) - else: + if not isinstance(mge, (AbstractSet, List, tuple)): raise malformed_mirror_groups_except() if not all(isinstance(value, str) for value in mge): @@ -726,50 +848,54 @@ def malformed_mirror_groups(): warnings.warn( ConfigurationWarning( "Ignoring {} in favor of {}".format( - self.settings._name("MIRROR_GROUPS"), - self.settings._name("MIRROR_GROUPS_EXCEPT"), + self.settings._prepend_prefix("MIRROR_GROUPS"), + self.settings._prepend_prefix("MIRROR_GROUPS_EXCEPT"), ) ) ) - mg = self.settings.MIRROR_GROUPS = None + + return None, frozenset(mge) if mg is not None: - if isinstance(mg, (bool, set, frozenset)): - pass - elif isinstance(mg, (list, tuple)): - mg = self.settings.MIRROR_GROUPS = frozenset(mg) - else: + if not isinstance(mg, (bool, AbstractSet, List, tuple)): raise malformed_mirror_groups() - if isinstance(mg, (set, frozenset)) and ( - not all(isinstance(value, str) for value in mg) - ): - raise malformed_mirror_groups() + if not isinstance(mg, bool): + if not all(isinstance(value, str) for value in mg): + raise malformed_mirror_groups() + + mg = frozenset(mg) + return mg, None + + return None, None - def _mirror_groups(self): + def _mirror_groups( + self, + normalized_mirror_groups: Optional[FrozenSet[str]], + normalized_mirror_groups_except: Optional[FrozenSet[str]], + ) -> None: """ Mirrors the user's LDAP groups in the Django database and updates the user's membership. """ + if self._user is None: + raise TypeError("The user should not be None") + target_group_names = frozenset(self._get_groups().get_group_names()) current_group_names = frozenset( self._user.groups.values_list("name", flat=True).iterator() ) - # These were normalized to sets above. - MIRROR_GROUPS_EXCEPT = self.settings.MIRROR_GROUPS_EXCEPT - MIRROR_GROUPS = self.settings.MIRROR_GROUPS - # If the settings are white- or black-listing groups, we'll update # target_group_names such that we won't modify the membership of groups # beyond our purview. - if isinstance(MIRROR_GROUPS_EXCEPT, (set, frozenset)): - target_group_names = (target_group_names - MIRROR_GROUPS_EXCEPT) | ( - current_group_names & MIRROR_GROUPS_EXCEPT - ) - elif isinstance(MIRROR_GROUPS, (set, frozenset)): - target_group_names = (target_group_names & MIRROR_GROUPS) | ( - current_group_names - MIRROR_GROUPS + if normalized_mirror_groups_except is not None: + target_group_names = ( + target_group_names - normalized_mirror_groups_except + ) | (current_group_names & normalized_mirror_groups_except) + elif normalized_mirror_groups is not None: + target_group_names = (target_group_names & normalized_mirror_groups) | ( + current_group_names - normalized_mirror_groups ) if target_group_names != current_group_names: @@ -790,7 +916,7 @@ def _mirror_groups(self): # Group information # - def _load_group_permissions(self): + def _load_group_permissions(self) -> None: """ Populates self._group_permissions based on LDAP group membership and Django group permissions. @@ -803,7 +929,7 @@ def _load_group_permissions(self): self._group_permissions = {"{}.{}".format(ct, name) for ct, name in perms} - def _get_groups(self): + def _get_groups(self) -> "_LDAPUserGroups": """ Returns an _LDAPUserGroups object, which can determine group membership. @@ -817,14 +943,14 @@ def _get_groups(self): # LDAP connection # - def _bind(self): + def _bind(self) -> None: """ Binds to the LDAP server with AUTH_LDAP_BIND_DN and AUTH_LDAP_BIND_PASSWORD. """ self._bind_as(self.settings.BIND_DN, self.settings.BIND_PASSWORD, sticky=True) - def _bind_as(self, bind_dn, bind_password, sticky=False): + def _bind_as(self, bind_dn: str, bind_password: str, sticky: bool = False) -> None: """ Binds to the LDAP server with the given credentials. This does not trap exceptions. @@ -833,37 +959,38 @@ def _bind_as(self, bind_dn, bind_password, sticky=False): the life of this object. If False, then the caller only wishes to test the credentials, after which the connection will be considered unbound. """ - self._get_connection().simple_bind_s(bind_dn, bind_password) + if bind_dn: + self._get_connection().rebind(bind_dn, bind_password) + else: + connection = self._get_connection() + # clear user and password, otherwise anonymous bind won't work + # https://github.com/cannatag/ldap3/issues/871 + connection.user = None + connection.password = None + connection.rebind(authentication=ldap3.ANONYMOUS) self._connection_bound = sticky - def _get_connection(self): + def _get_connection(self) -> Connection: """ - Returns our cached LDAPObject, which may or may not be bound. + Returns our cached Connection, which may or may not be bound. """ if self._connection is None: - uri = self.settings.SERVER_URI - if callable(uri): - if func_supports_parameter(uri, "request"): - uri = uri(self._request) - else: - warnings.warn( - "Update AUTH_LDAP_SERVER_URI callable %s.%s to accept " - "a positional `request` argument. Support for callables " - "accepting no arguments will be removed in a future " - "version." % (uri.__module__, uri.__name__), - DeprecationWarning, - ) - uri = uri() - - self._connection = self.backend.ldap.initialize(uri, bytes_mode=False) - - for opt, value in self.settings.CONNECTION_OPTIONS.items(): - self._connection.set_option(opt, value) + server = self.backend.get_server(self._request) + self._connection = Connection( + server, + client_strategy=ldap3.ASYNC, + lazy=False, + raise_exceptions=True, + ) - if self.settings.START_TLS: + if ( + not self._connection.tls_started + and not self._connection.starting_tls + and (self.settings.START_TLS or self._connection.server.ssl) + ): logger.debug("Initiating TLS") - self._connection.start_tls_s() + self._connection.start_tls() return self._connection @@ -873,37 +1000,39 @@ class _LDAPUserGroups: Represents the set of groups that a user belongs to. """ - def __init__(self, ldap_user): - self.settings = ldap_user.settings - self._ldap_user = ldap_user - self._group_type = None - self._group_search = None - self._group_infos = None - self._group_dns = None - self._group_names = None + def __init__(self, ldap_user: _LDAPUser) -> None: + self.settings: LDAPSettings = ldap_user.settings + self._ldap_user: _LDAPUser = ldap_user + self._group_type: Optional[LDAPGroupType] = None + self._group_search: Optional[AbstractLDAPSearch] = None + self._group_infos: Optional[Collection[Tuple[str, Dict[str, Any]]]] = None + self._group_dns: Optional[Set[str]] = None + self._group_names: Optional[Set[str]] = None self._init_group_settings() - def _init_group_settings(self): + def _init_group_settings(self) -> None: """ Loads the settings we need to deal with groups. Raises ImproperlyConfigured if anything's not right. - """ + self._group_type = self.settings.GROUP_TYPE - if self._group_type is None: + if not isinstance(self._group_type, LDAPGroupType): raise ImproperlyConfigured( - "AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance." + "%s must be an LDAPGroupType instance." + % self.settings._prepend_prefix("GROUP_TYPE") ) self._group_search = self.settings.GROUP_SEARCH - if self._group_search is None: + if not isinstance(self._group_search, AbstractLDAPSearch): raise ImproperlyConfigured( - "AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance." + "%s must be an LDAPSearch instance." + % self.settings._prepend_prefix("GROUP_SEARCH") ) - def get_group_names(self): + def get_group_names(self) -> Set[str]: """ Returns the set of Django group names that this user belongs to by virtue of LDAP group memberships. @@ -912,30 +1041,39 @@ def get_group_names(self): self._load_cached_attr("_group_names") if self._group_names is None: + if self._group_type is None: + raise TypeError("The group type should not be None") + group_infos = self._get_group_infos() - self._group_names = { + group_names = { self._group_type.group_name_from_info(group_info) for group_info in group_infos } + if None in group_names: + group_names.remove(None) + self._group_names = cast(Set[str], group_names) self._cache_attr("_group_names") return self._group_names - def is_member_of(self, group_dn): + def is_member_of(self, group_dn: str) -> bool: """ Returns true if our user is a member of the given group. """ - is_member = None - # Normalize the DN group_dn = group_dn.lower() # If we have self._group_dns, we'll use it. Otherwise, we'll try to # avoid the cost of loading it. - if self._group_dns is None: + if self._group_dns is None and isinstance( + self._group_type, LDAPGroupTypeIsMember + ): + if self._group_type is None: + raise TypeError("The group type should not be None") + is_member = self._group_type.is_member(self._ldap_user, group_dn) - if is_member is None: + else: is_member = group_dn in self.get_group_dns() logger.debug( @@ -946,7 +1084,7 @@ def is_member_of(self, group_dn): return is_member - def get_group_dns(self): + def get_group_dns(self) -> Set[str]: """ Returns a (cached) set of the distinguished names in self._group_infos. """ @@ -956,31 +1094,38 @@ def get_group_dns(self): return self._group_dns - def _get_group_infos(self): + def _get_group_infos( + self, + ) -> Collection[Tuple[str, Dict[str, Any]]]: """ Returns a (cached) list of group_info structures for the groups that our user is a member of. """ if self._group_infos is None: + if self._group_type is None: + raise TypeError("The group type should not be None") + if self._group_search is None: + raise TypeError("The group search should not be None") + self._group_infos = self._group_type.user_groups( self._ldap_user, self._group_search ) return self._group_infos - def _load_cached_attr(self, attr_name): + def _load_cached_attr(self, attr_name: str) -> None: if self.settings.CACHE_TIMEOUT > 0: key = self._cache_key(attr_name) value = cache.get(key) setattr(self, attr_name, value) - def _cache_attr(self, attr_name): + def _cache_attr(self, attr_name: str) -> None: if self.settings.CACHE_TIMEOUT > 0: key = self._cache_key(attr_name) value = getattr(self, attr_name, None) cache.set(key, value, self.settings.CACHE_TIMEOUT) - def _cache_key(self, attr_name): + def _cache_key(self, attr_name: str) -> str: """ Memcache keys can't have spaces in them, so we'll remove them from the DN for maximum compatibility. @@ -998,70 +1143,106 @@ class LDAPSettings: if they are not specified by the configuration. """ - _prefix = "AUTH_LDAP_" - - defaults = { - "ALWAYS_UPDATE_USER": True, - "AUTHORIZE_ALL_USERS": False, - "BIND_AS_AUTHENTICATING_USER": False, - "BIND_DN": "", - "BIND_PASSWORD": "", - "CONNECTION_OPTIONS": {}, - "DENY_GROUP": None, - "FIND_GROUP_PERMS": False, - "CACHE_TIMEOUT": 0, - "GROUP_SEARCH": None, - "GROUP_TYPE": None, - "MIRROR_GROUPS": None, - "MIRROR_GROUPS_EXCEPT": None, - "PERMIT_EMPTY_PASSWORD": False, - "REQUIRE_GROUP": None, - "NO_NEW_USERS": False, - "SERVER_URI": "ldap://localhost", - "START_TLS": False, - "USER_QUERY_FIELD": None, - "USER_ATTRLIST": None, - "USER_ATTR_MAP": {}, - "USER_DN_TEMPLATE": None, - "USER_FLAGS_BY_GROUP": {}, - "USER_SEARCH": None, - } - - def __init__(self, prefix="AUTH_LDAP_", defaults={}): + prefix: str = "AUTH_LDAP_" + + def __init__( + self, + always_update_user: bool = True, + authorize_all_users: bool = False, + bind_as_authenticating_user: bool = False, + bind_dn: str = "", + bind_password: str = "", + cache_timeout: int = 0, + deny_group: Optional[str] = None, + find_group_perms: bool = False, + group_search: Optional[AbstractLDAPSearch] = None, + group_type: Optional[LDAPGroupType] = None, + mirror_groups: Union[bool, Collection[str], None] = None, + mirror_groups_except: Optional[Collection[str]] = None, + no_new_users: bool = False, + permit_empty_password: bool = False, + require_group: Union[str, LDAPGroupQuery, None] = None, + server_uri: Union[ + str, Callable[[Optional[HttpRequest]], str] + ] = "ldap://localhost", + start_tls: bool = False, + user_attrlist: Optional[Collection[str]] = None, + user_attr_map: Optional[Dict[str, str]] = None, + user_dn_template: Optional[str] = None, + user_flags_by_group: Optional[Dict[str, Union[str, LDAPGroupQuery]]] = None, + user_query_field: Optional[str] = None, + user_search: Optional[AbstractLDAPSearch] = None, + ) -> None: """ Loads our settings from django.conf.settings, applying defaults for any that are omitted. """ - self._prefix = prefix - - defaults = dict(self.defaults, **defaults) - - for name, default in defaults.items(): - value = getattr(django.conf.settings, prefix + name, default) - setattr(self, name, value) + self.ALWAYS_UPDATE_USER: bool = self._get_setting( + "ALWAYS_UPDATE_USER", always_update_user + ) + self.AUTHORIZE_ALL_USERS: bool = self._get_setting( + "AUTHORIZE_ALL_USERS", authorize_all_users + ) + self.BIND_AS_AUTHENTICATING_USER: bool = self._get_setting( + "BIND_AS_AUTHENTICATING_USER", bind_as_authenticating_user + ) + self.BIND_DN: str = self._get_setting("BIND_DN", bind_dn) + self.BIND_PASSWORD: str = self._get_setting("BIND_PASSWORD", bind_password) + self.CACHE_TIMEOUT: int = self._get_setting("CACHE_TIMEOUT", cache_timeout) + self.DENY_GROUP: Optional[str] = self._get_setting("DENY_GROUP", deny_group) + self.FIND_GROUP_PERMS: bool = self._get_setting( + "FIND_GROUP_PERMS", find_group_perms + ) + self.GROUP_SEARCH: Optional[AbstractLDAPSearch] = self._get_setting( + "GROUP_SEARCH", group_search + ) + self.GROUP_TYPE: Optional[LDAPGroupType] = self._get_setting( + "GROUP_TYPE", group_type + ) + self.MIRROR_GROUPS: Union[bool, Collection[str], None] = self._get_setting( + "MIRROR_GROUPS", mirror_groups + ) + self.MIRROR_GROUPS_EXCEPT: Optional[Collection[str]] = self._get_setting( + "MIRROR_GROUPS_EXCEPT", mirror_groups_except + ) + self.NO_NEW_USERS: bool = self._get_setting("NO_NEW_USERS", no_new_users) + self.PERMIT_EMPTY_PASSWORD: bool = self._get_setting( + "PERMIT_EMPTY_PASSWORD", permit_empty_password + ) + self.REQUIRE_GROUP: Union[str, LDAPGroupQuery, None] = self._get_setting( + "REQUIRE_GROUP", require_group + ) + self.SERVER_URI: Union[ + str, Callable[[Optional[HttpRequest]], str] + ] = self._get_setting("SERVER_URI", server_uri) + self.START_TLS: bool = self._get_setting("START_TLS", start_tls) + self.USER_ATTRLIST: Optional[Collection[str]] = self._get_setting( + "USER_ATTRLIST", user_attrlist + ) + self.USER_ATTR_MAP: Dict[str, str] = self._get_setting( + "USER_ATTR_MAP", user_attr_map or dict() + ) + self.USER_DN_TEMPLATE: Optional[str] = self._get_setting( + "USER_DN_TEMPLATE", user_dn_template + ) + self.USER_FLAGS_BY_GROUP: Dict[ + str, Union[str, LDAPGroupQuery] + ] = self._get_setting("USER_FLAGS_BY_GROUP", user_flags_by_group or dict()) + self.USER_QUERY_FIELD: Optional[str] = self._get_setting( + "USER_QUERY_FIELD", user_query_field + ) + self.USER_SEARCH: Optional[AbstractLDAPSearch] = self._get_setting( + "USER_SEARCH", user_search + ) - # Compatibility with old caching settings. - if getattr( - django.conf.settings, - self._name("CACHE_GROUPS"), - defaults.get("CACHE_GROUPS"), - ): - warnings.warn( - "Found deprecated setting AUTH_LDAP_CACHE_GROUP. Use " - "AUTH_LDAP_CACHE_TIMEOUT instead.", - DeprecationWarning, - ) - self.CACHE_TIMEOUT = getattr( - django.conf.settings, - self._name("GROUP_CACHE_TIMEOUT"), - defaults.get("GROUP_CACHE_TIMEOUT", 3600), - ) + def _prepend_prefix(self, suffix: str) -> str: + return self.prefix + suffix - def _name(self, suffix): - return self._prefix + suffix + def _get_setting(self, suffix: str, default: T) -> T: + return getattr(django.conf.settings, self._prepend_prefix(suffix), default) -def valid_cache_key(key): +def valid_cache_key(key: str) -> str: """ Sanitizes a cache key for memcached. """ diff --git a/django_auth_ldap/config.py b/django_auth_ldap/config.py index 7ac387dc..180e79af 100644 --- a/django_auth_ldap/config.py +++ b/django_auth_ldap/config.py @@ -31,10 +31,36 @@ import logging import pprint +from abc import ABC, abstractmethod +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Collection, + Dict, + ItemsView, + Iterable, + Iterator, + List, + Mapping, + Optional, + Set, + Tuple, + Union, +) -import ldap -import ldap.filter from django.utils.tree import Node +from ldap3 import ALL_ATTRIBUTES, Connection +from ldap3.core.exceptions import ( + LDAPException, + LDAPNoSuchAttributeResult, + LDAPUndefinedAttributeTypeResult, +) +from ldap3.core.results import RESULT_COMPARE_TRUE +from ldap3.utils.conv import escape_filter_chars + +if TYPE_CHECKING: + from django_auth_ldap.backend import _LDAPUser, _LDAPUserGroups class ConfigurationWarning(UserWarning): @@ -46,286 +72,292 @@ class _LDAPConfig: A private class that loads and caches some global objects. """ - logger = None - - _ldap_configured = False - - @classmethod - def get_ldap(cls, global_options=None): - """ - Returns the configured ldap module. - """ - # Apply global LDAP options once - if not cls._ldap_configured and global_options is not None: - for opt, value in global_options.items(): - ldap.set_option(opt, value) - - cls._ldap_configured = True - - return ldap + _logger: Optional[logging.Logger] = None @classmethod - def get_logger(cls): + def get_logger(cls) -> logging.Logger: """ Initializes and returns our logger instance. """ - if cls.logger is None: - cls.logger = logging.getLogger("django_auth_ldap") - cls.logger.addHandler(logging.NullHandler()) + if cls._logger is None: + cls._logger = logging.getLogger("django_auth_ldap") + cls._logger.addHandler(logging.NullHandler()) - return cls.logger + return cls._logger # Our global logger -logger = _LDAPConfig.get_logger() +logger: logging.Logger = _LDAPConfig.get_logger() -class LDAPSearch: +class AbstractLDAPSearch(ABC): """ - Public class that holds a set of LDAP search parameters. Objects of this - class should be considered immutable. Only the initialization method is - documented for configuration purposes. Internal clients may use the other - methods to refine and execute the search. + The abstract base class for ldap searches. """ - def __init__(self, base_dn, scope, filterstr="(objectClass=*)", attrlist=None): - """ - These parameters are the same as the first three parameters to - ldap.search_s. - """ - self.base_dn = base_dn - self.scope = scope - self.filterstr = filterstr - self.attrlist = attrlist - self.ldap = _LDAPConfig.get_ldap() - - def __repr__(self): - return "<{}: {}>".format(type(self).__name__, self.base_dn) - - def search_with_additional_terms(self, term_dict, escape=True): + @abstractmethod + def search_with_additional_terms( + self, term_dict: Dict[str, str], escape: bool = True + ) -> "AbstractLDAPSearch": """ Returns a new search object with additional search terms and-ed to the filter string. term_dict maps attribute names to assertion values. If you don't want the values escaped, pass escape=False. """ - term_strings = [self.filterstr] - - for name, value in term_dict.items(): - if escape: - value = self.ldap.filter.escape_filter_chars(value) - term_strings.append("({}={})".format(name, value)) + pass - filterstr = "(&{})".format("".join(term_strings)) - - return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) - - def search_with_additional_term_string(self, filterstr): + @abstractmethod + def search_with_additional_term_string( + self, filterstr: str + ) -> "AbstractLDAPSearch": """ Returns a new search object with filterstr and-ed to the original filter string. The caller is responsible for passing in a properly escaped string. """ - filterstr = "(&{}{})".format(self.filterstr, filterstr) + pass - return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) - - def execute(self, connection, filterargs=(), escape=True): + def execute( + self, + connection: Connection, + filterargs: Union[Tuple[Any, ...], Mapping[str, Any]] = (), + escape: bool = True, + ) -> Collection[Tuple[str, Dict[str, Any]]]: """ - Executes the search on the given connection (an LDAPObject). filterargs + Executes the search on the given connection. filterargs is an object that will be used for expansion of the filter string. If escape is True, values in filterargs will be escaped. - - The python-ldap library returns utf8-encoded strings. For the sake of - sanity, this method will decode all result strings and return them as - Unicode. """ - if escape: - filterargs = self._escape_filterargs(filterargs) + self._search(connection, filterargs, escape) - try: - filterstr = self.filterstr % filterargs - results = connection.search_s( - self.base_dn, self.scope, filterstr, self.attrlist - ) - except ldap.LDAPError as e: - results = [] - logger.error( - "search_s('{}', {}, '{}') raised {}".format( - self.base_dn, self.scope, filterstr, pprint.pformat(e) - ) - ) + return self._response(connection) - return self._process_results(results) + @abstractmethod + def _abandon(self, connection: Connection) -> None: + """ + Abandon a previous asynchronous search. + """ + pass - def _begin(self, connection, filterargs=(), escape=True): + @abstractmethod + def _search( + self, + connection: Connection, + filterargs: Union[Tuple[Any, ...], Mapping[str, Any]] = (), + escape: bool = True, + ) -> None: """ Begins an asynchronous search and returns the message id to retrieve the results. filterargs is an object that will be used for expansion of the filter string. If escape is True, values in filterargs will be escaped. - """ - if escape: - filterargs = self._escape_filterargs(filterargs) - - try: - filterstr = self.filterstr % filterargs - msgid = connection.search( - self.base_dn, self.scope, filterstr, self.attrlist - ) - except ldap.LDAPError as e: - msgid = None - logger.error( - "search('{}', {}, '{}') raised {}".format( - self.base_dn, self.scope, filterstr, pprint.pformat(e) - ) - ) - - return msgid + pass - def _results(self, connection, msgid): + @abstractmethod + def _response( + self, connection: Connection + ) -> Collection[Tuple[str, Dict[str, Any]]]: """ - Returns the result of a previous asynchronous query. - """ - try: - kind, results = connection.result(msgid) - if kind not in (ldap.RES_SEARCH_ENTRY, ldap.RES_SEARCH_RESULT): - results = [] - except ldap.LDAPError as e: - results = [] - logger.error("result({}) raised {}".format(msgid, pprint.pformat(e))) + Returns the result of a previous asynchronous query or an empty array + if no search has been initiated. - return self._process_results(results) + The python-ldap library returns utf8-encoded strings. For the sake of + sanity, this method will decode all result strings and return them as + Unicode. + """ + pass - def _escape_filterargs(self, filterargs): + @classmethod + def _escape_filterargs( + cls, filterargs: Union[Tuple[Any, ...], Mapping[str, Any]] + ) -> Union[Tuple[Any, ...], Mapping[str, Any]]: """ - Escapes values in filterargs. + Escapes all string values in filterargs and all others remain the same. filterargs is a value suitable for Django's string formatting operator (%), which means it's either a tuple or a dict. This return a new tuple or dict with all values escaped for use in filter strings. - """ if isinstance(filterargs, tuple): - filterargs = tuple( - self.ldap.filter.escape_filter_chars(value) for value in filterargs - ) - elif isinstance(filterargs, dict): - filterargs = { - key: self.ldap.filter.escape_filter_chars(value) + filterargs = tuple(escape_filter_chars(str(value)) for value in filterargs) + elif isinstance(filterargs, Mapping): + filterargs = dict( + (key, escape_filter_chars(str(value))) for key, value in filterargs.items() - } + ) else: - raise TypeError("filterargs must be a tuple or dict.") + raise TypeError("filterargs must be a tuple or mapping.") return filterargs - def _process_results(self, results): + +class LDAPSearch(AbstractLDAPSearch): + """ + Public class that holds a set of LDAP search parameters. Objects of this + class should be considered immutable. Only the initialization method is + documented for configuration purposes. Internal clients may use the other + methods to refine and execute the search. + """ + + def __init__( + self, + base_dn: str, + scope: str, + filterstr: str = "(objectClass=*)", + attrlist: Optional[Collection[str]] = None, + ) -> None: """ - Returns a sanitized copy of raw LDAP results. This scrubs out - references, decodes utf8, normalizes DNs, etc. + These parameters are the same as the first three parameters to + ldap.search_s. """ - results = [r for r in results if r[0] is not None] - results = _DeepStringCoder("utf-8").decode(results) + self.base_dn: str = base_dn + self.scope: str = scope + self.filterstr: str = filterstr + self.attrlist: Optional[Collection[str]] = attrlist + self.msgid: Optional[int] = None + + def __repr__(self) -> str: + return "<{}: {}>".format(type(self).__name__, self.base_dn) + + def search_with_additional_terms( + self, term_dict: Dict[str, str], escape: bool = True + ) -> "LDAPSearch": + term_strings = [self.filterstr] + + for name, value in term_dict.items(): + if escape: + value = escape_filter_chars(value) + term_strings.append("({}={})".format(name, value)) + + filterstr = "(&{})".format("".join(term_strings)) + + return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) + + def search_with_additional_term_string(self, filterstr: str) -> "LDAPSearch": + filterstr = "(&{}{})".format(self.filterstr, filterstr) + + return type(self)(self.base_dn, self.scope, filterstr, attrlist=self.attrlist) + + def _abandon(self, connection: Connection) -> None: + if self.msgid is not None: + connection.abandon(self.msgid) + self.msgid = None + + def _search( + self, + connection: Connection, + filterargs: Union[Tuple[Any, ...], Mapping[str, Any]] = (), + escape: bool = True, + ) -> None: + self._abandon(connection) - # The normal form of a DN is lower case. - results = [(r[0].lower(), r[1]) for r in results] + if escape: + filterargs = self._escape_filterargs(filterargs) + + filterstr = self.filterstr % filterargs - result_dns = [result[0] for result in results] - logger.debug( - "search_s('{}', {}, '{}') returned {} objects: {}".format( + attrlist = self.attrlist + if attrlist is None: + attrlist = ALL_ATTRIBUTES + + try: + self.msgid = connection.search( self.base_dn, + filterstr, self.scope, - self.filterstr, - len(result_dns), - "; ".join(result_dns), + attributes=attrlist, + ) + except LDAPException as e: + logger.error( + "search('{}', {}, '{}') raised {}".format( + self.base_dn, self.scope, filterstr, pprint.pformat(e) + ) ) - ) - return results + def _response(self, connection: Connection) -> List[Tuple[str, Dict[str, Any]]]: + if self.msgid is None: + raise RuntimeError( + "The search has either not been initiated, abandoned," + " or the results have already been fetched" + ) + try: + response = [ + (entry["dn"], entry["attributes"]) + for entry in connection.get_response(self.msgid)[0] + ] + response_dns = [entry[0] for entry in response] + # check encoding and no None DNs and lower case Dns + + logger.debug( + "search('{}', {}, '{}') returned {} objects: {}".format( + self.base_dn, + self.scope, + self.filterstr, + len(response_dns), + "; ".join(response_dns), + ) + ) + except LDAPException as e: + response = [] + logger.error("result({}) raised {}".format(self.msgid, pprint.pformat(e))) -class LDAPSearchUnion: + self.msgid = None + + return response + + +class LDAPSearchUnion(AbstractLDAPSearch): """ A compound search object that returns the union of the results. Instantiate - it with one or more LDAPSearch objects. + it with one or more AbstractLDAPSearch objects. """ - def __init__(self, *args): - self.searches = args - self.ldap = _LDAPConfig.get_ldap() + def __init__(self, *args: AbstractLDAPSearch) -> None: + self.searches: Tuple[AbstractLDAPSearch, ...] = args - def search_with_additional_terms(self, term_dict, escape=True): - searches = [ + def search_with_additional_terms( + self, term_dict: Dict[str, str], escape: bool = True + ) -> "LDAPSearchUnion": + searches = tuple( s.search_with_additional_terms(term_dict, escape) for s in self.searches - ] + ) return type(self)(*searches) - def search_with_additional_term_string(self, filterstr): - searches = [ + def search_with_additional_term_string(self, filterstr: str) -> "LDAPSearchUnion": + searches = tuple( s.search_with_additional_term_string(filterstr) for s in self.searches - ] + ) return type(self)(*searches) - def execute(self, connection, filterargs=(), escape=True): - msgids = [ - search._begin(connection, filterargs, escape) for search in self.searches - ] - results = {} - - for search, msgid in zip(self.searches, msgids): - if msgid is not None: - result = search._results(connection, msgid) - results.update(dict(result)) + def _abandon(self, connection: Connection) -> None: + for search in self.searches: + search._abandon(connection) - return results.items() + def _search( + self, + connection: Connection, + filterargs: Union[Tuple[Any, ...], Mapping[str, Any]] = (), + escape: bool = True, + ) -> None: + for search in self.searches: + search._search(connection, filterargs, escape) + def _response(self, connection: Connection) -> ItemsView[str, Dict[str, Any]]: + results = dict() -class _DeepStringCoder: - """ - Encodes and decodes strings in a nested structure of lists, tuples, and - dicts. This is helpful when interacting with the Unicode-unaware - python-ldap. - """ + for search in self.searches: + result = search._response(connection) + results.update(dict(result)) - def __init__(self, encoding): - self.encoding = encoding - self.ldap = _LDAPConfig.get_ldap() - - def decode(self, value): - try: - if isinstance(value, bytes): - value = value.decode(self.encoding) - elif isinstance(value, list): - value = self._decode_list(value) - elif isinstance(value, tuple): - value = tuple(self._decode_list(value)) - elif isinstance(value, dict): - value = self._decode_dict(value) - except UnicodeDecodeError: - pass - - return value - - def _decode_list(self, value): - return [self.decode(v) for v in value] - - def _decode_dict(self, value): - # Attribute dictionaries should be case-insensitive. python-ldap - # defines this, although for some reason, it doesn't appear to use it - # for search results. - decoded = self.ldap.cidict.cidict() - - for k, v in value.items(): - decoded[self.decode(k)] = self.decode(v) - - return decoded + return results.items() -class LDAPGroupType: +class LDAPGroupType(ABC): """ This is an abstract base class for classes that determine LDAP group membership. A group can mean many different things in LDAP, so we will need @@ -335,46 +367,29 @@ class LDAPGroupType: name_attr is the name of the LDAP attribute from which we will take the Django group name. - - Subclasses in this file must use self.ldap to access the python-ldap module. - This will be a mock object during unit tests. """ - def __init__(self, name_attr="cn"): - self.name_attr = name_attr - self.ldap = _LDAPConfig.get_ldap() + def __init__(self, name_attr: str = "cn") -> None: + self.name_attr: str = name_attr - def user_groups(self, ldap_user, group_search): + @abstractmethod + def user_groups( + self, ldap_user: "_LDAPUser", group_search: AbstractLDAPSearch + ) -> Collection[Tuple[str, Dict[str, Any]]]: """ Returns a list of group_info structures, each one a group to which ldap_user belongs. group_search is an LDAPSearch object that returns all of the groups that the user might belong to. Typical implementations will apply additional filters to group_search and return the results of - the search. ldap_user represents the user and has the following three - properties: - - dn: the distinguished name - attrs: a dictionary of LDAP attributes (with lists of values) - connection: an LDAPObject that has been bound with credentials + the search. This is the primitive method in the API and must be implemented. """ - return [] - - def is_member(self, ldap_user, group_dn): - """ - This method is an optimization for determining group membership without - loading all of the user's groups. Subclasses that are able to do this - may return True or False. ldap_user is as above. group_dn is the - distinguished name of the group in question. - - The base implementation returns None, which means we don't have enough - information. The caller will have to call user_groups() instead and look - for group_dn in the results. - """ - return None + pass - def group_name_from_info(self, group_info): + def group_name_from_info( + self, group_info: Tuple[str, Dict[str, Any]] + ) -> Optional[str]: """ Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of the Django group. This may return None to indicate that a particular @@ -385,68 +400,85 @@ def group_name_from_info(self, group_info): parameter. """ try: - name = group_info[1][self.name_attr][0] + return group_info[1][self.name_attr][0] except (KeyError, IndexError): - name = None + return None - return name + +class LDAPGroupTypeIsMember(LDAPGroupType): + @abstractmethod + def is_member(self, ldap_user: "_LDAPUser", group_dn: str) -> bool: + """ + Returns True if the group is the user's primary group or if the user is + listed in the group's memberUid attribute. + """ + pass -class PosixGroupType(LDAPGroupType): +class PosixGroupType(LDAPGroupTypeIsMember): """ An LDAPGroupType subclass that handles groups of class posixGroup. """ - def user_groups(self, ldap_user, group_search): + def user_groups( + self, ldap_user: "_LDAPUser", group_search: AbstractLDAPSearch + ) -> Collection[Tuple[str, Dict[str, Any]]]: """ Searches for any group that is either the user's primary or contains the user as a member. """ - groups = [] + if ldap_user.attrs is None: + raise TypeError("The attrs of the LDAP user should not be None") try: - user_uid = ldap_user.attrs["uid"][0] + user_uid = ldap_user.get_single_attr("uid") if "gidNumber" in ldap_user.attrs: - user_gid = ldap_user.attrs["gidNumber"][0] + user_gid = ldap_user.get_single_attr("gidNumber") filterstr = "(|(gidNumber={})(memberUid={}))".format( - self.ldap.filter.escape_filter_chars(user_gid), - self.ldap.filter.escape_filter_chars(user_uid), + escape_filter_chars(user_gid), + escape_filter_chars(user_uid), ) else: - filterstr = "(memberUid={})".format( - self.ldap.filter.escape_filter_chars(user_uid) - ) + filterstr = "(memberUid={})".format(escape_filter_chars(user_uid)) search = group_search.search_with_additional_term_string(filterstr) groups = search.execute(ldap_user.connection) except (KeyError, IndexError): - pass + groups = [] return groups - def is_member(self, ldap_user, group_dn): + def is_member(self, ldap_user: "_LDAPUser", group_dn: str) -> bool: """ Returns True if the group is the user's primary group or if the user is listed in the group's memberUid attribute. """ try: - user_uid = ldap_user.attrs["uid"][0] + user_uid = ldap_user.get_single_attr("uid") try: - is_member = ldap_user.connection.compare_s( + msgid = ldap_user.connection.compare( group_dn, "memberUid", user_uid.encode() ) - except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): + is_member = ( + ldap_user.connection.get_response(msgid)[1]["result"] + == RESULT_COMPARE_TRUE + ) + except (LDAPUndefinedAttributeTypeResult, LDAPNoSuchAttributeResult): is_member = False if not is_member: try: - user_gid = ldap_user.attrs["gidNumber"][0] - is_member = ldap_user.connection.compare_s( + user_gid = ldap_user.get_single_attr("gidNumber") + msgid = ldap_user.connection.compare( group_dn, "gidNumber", user_gid.encode() ) - except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): + is_member = ( + ldap_user.connection.get_response(msgid)[1]["result"] + == RESULT_COMPARE_TRUE + ) + except (LDAPUndefinedAttributeTypeResult, LDAPNoSuchAttributeResult): is_member = False except (KeyError, IndexError): is_member = False @@ -454,84 +486,104 @@ def is_member(self, ldap_user, group_dn): return is_member -class MemberDNGroupType(LDAPGroupType): +class MemberDNGroupType(LDAPGroupTypeIsMember): """ A group type that stores lists of members as distinguished names. """ - def __init__(self, member_attr, name_attr="cn"): + def __init__(self, member_attr: str, name_attr: str = "cn") -> None: """ member_attr is the attribute on the group object that holds the list of member DNs. """ - self.member_attr = member_attr + self.member_attr: str = member_attr super().__init__(name_attr) def __repr__(self): return "<{}: {}>".format(type(self).__name__, self.member_attr) - def user_groups(self, ldap_user, group_search): + def user_groups( + self, ldap_user: "_LDAPUser", group_search: AbstractLDAPSearch + ) -> Collection[Tuple[str, Dict[str, Any]]]: + if ldap_user.dn is None: + raise TypeError("The dn of the LDAP user should not be None") + search = group_search.search_with_additional_terms( {self.member_attr: ldap_user.dn} ) return search.execute(ldap_user.connection) - def is_member(self, ldap_user, group_dn): + def is_member(self, ldap_user: "_LDAPUser", group_dn: str) -> bool: + if ldap_user.dn is None: + raise TypeError("The dn of the LDAP user should not be None") + try: - result = ldap_user.connection.compare_s( + msgid = ldap_user.connection.compare( group_dn, self.member_attr, ldap_user.dn.encode() ) - except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): - result = 0 - - return result + return ( + ldap_user.connection.get_response(msgid)[1]["result"] + == RESULT_COMPARE_TRUE + ) + except (LDAPUndefinedAttributeTypeResult, LDAPNoSuchAttributeResult): + return False class NestedMemberDNGroupType(LDAPGroupType): """ A group type that stores lists of members as distinguished names and - supports nested groups. There is no shortcut for is_member in this case, so - it's left unimplemented. + supports nested groups. """ - def __init__(self, member_attr, name_attr="cn"): + def __init__(self, member_attr: str, name_attr: str = "cn") -> None: """ member_attr is the attribute on the group object that holds the list of member DNs. """ - self.member_attr = member_attr + self.member_attr: str = member_attr super().__init__(name_attr) - def user_groups(self, ldap_user, group_search): + def user_groups( + self, ldap_user: "_LDAPUser", group_search: AbstractLDAPSearch + ) -> ItemsView[str, Dict[str, Any]]: """ This searches for all of a user's groups from the bottom up. In other words, it returns the groups that the user belongs to, the groups that those groups belong to, etc. Circular references will be detected and pruned. """ - group_info_map = {} # Maps group_dn to group_info of groups we've found + if ldap_user.dn is None: + raise TypeError("The dn of the LDAP user should not be None") + + group_info_map = dict() # Maps group_dn to group_info of groups we've found member_dn_set = {ldap_user.dn} # Member DNs to search with next handled_dn_set = set() # Member DNs that we've already searched with while len(member_dn_set) > 0: - group_infos = self.find_groups_with_any_member( - member_dn_set, group_search, ldap_user.connection + group_infos = dict( + self._find_groups_with_any_member( + member_dn_set, group_search, ldap_user.connection + ) ) - new_group_info_map = {info[0]: info for info in group_infos} - group_info_map.update(new_group_info_map) + group_info_map.update(group_infos) handled_dn_set.update(member_dn_set) # Get ready for the next iteration. To avoid cycles, we make sure # never to search with the same member DN twice. - member_dn_set = set(new_group_info_map.keys()) - handled_dn_set + member_dn_set = set(group_infos.keys()) - handled_dn_set - return group_info_map.values() + return group_info_map.items() - def find_groups_with_any_member(self, member_dn_set, group_search, connection): + def _find_groups_with_any_member( + self, + member_dn_set: Set[str], + group_search: AbstractLDAPSearch, + connection: Connection, + ) -> Collection[Tuple[str, Dict[str, Any]]]: terms = [ - "({}={})".format(self.member_attr, self.ldap.filter.escape_filter_chars(dn)) + "({}={})".format(self.member_attr, escape_filter_chars(dn)) for dn in member_dn_set ] @@ -546,7 +598,7 @@ class GroupOfNamesType(MemberDNGroupType): An LDAPGroupType subclass that handles groups of class groupOfNames. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("member", name_attr) @@ -556,7 +608,7 @@ class NestedGroupOfNamesType(NestedMemberDNGroupType): nested group references. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("member", name_attr) @@ -565,7 +617,7 @@ class GroupOfUniqueNamesType(MemberDNGroupType): An LDAPGroupType subclass that handles groups of class groupOfUniqueNames. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("uniqueMember", name_attr) @@ -575,7 +627,7 @@ class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType): with nested group references. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("uniqueMember", name_attr) @@ -584,7 +636,7 @@ class ActiveDirectoryGroupType(MemberDNGroupType): An LDAPGroupType subclass that handles Active Directory groups. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("member", name_attr) @@ -594,7 +646,7 @@ class NestedActiveDirectoryGroupType(NestedMemberDNGroupType): group references. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("member", name_attr) @@ -603,7 +655,7 @@ class OrganizationalRoleGroupType(MemberDNGroupType): An LDAPGroupType subclass that handles groups of class organizationalRole. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("roleOccupant", name_attr) @@ -613,7 +665,7 @@ class NestedOrganizationalRoleGroupType(NestedMemberDNGroupType): with nested group references. """ - def __init__(self, name_attr="cn"): + def __init__(self, name_attr: str = "cn") -> None: super().__init__("roleOccupant", name_attr) @@ -626,8 +678,7 @@ class LDAPGroupQuery(Node): group DN as the only argument. These queries can then be combined with the ``&``, ``|``, and ``~`` operators. - :param str group_dn: The DN of a group to test for membership. - + :param str group_dns: The DN of a group to test for membership. """ # Connection types @@ -637,23 +688,23 @@ class LDAPGroupQuery(Node): _CONNECTORS = [AND, OR] - def __init__(self, *args, **kwargs): - super().__init__(children=list(args) + list(kwargs.items())) + def __init__(self, *group_dns: str) -> None: + super().__init__(children=list(group_dns)) - def __and__(self, other): + def __and__(self, other: "LDAPGroupQuery") -> "LDAPGroupQuery": return self._combine(other, self.AND) - def __or__(self, other): + def __or__(self, other: "LDAPGroupQuery") -> "LDAPGroupQuery": return self._combine(other, self.OR) - def __invert__(self): + def __invert__(self) -> "LDAPGroupQuery": obj = type(self)() obj.add(self, self.AND) obj.negate() return obj - def _combine(self, other, conn): + def _combine(self, other: "LDAPGroupQuery", conn: str) -> "LDAPGroupQuery": if not isinstance(other, LDAPGroupQuery): raise TypeError(other) if conn not in self._CONNECTORS: @@ -666,7 +717,9 @@ def _combine(self, other, conn): return obj - def resolve(self, ldap_user, groups=None): + def resolve( + self, ldap_user: "_LDAPUser", groups: Optional["_LDAPUserGroups"] = None + ) -> bool: if groups is None: groups = ldap_user._get_groups() @@ -677,7 +730,7 @@ def resolve(self, ldap_user, groups=None): return result @property - def aggregator(self): + def aggregator(self) -> Callable[[Iterable[object]], bool]: """ Returns a function for aggregating a sequence of sub-results. """ @@ -690,7 +743,9 @@ def aggregator(self): return aggregator - def _resolve_children(self, ldap_user, groups): + def _resolve_children( + self, ldap_user: "_LDAPUser", groups: "_LDAPUserGroups" + ) -> Iterator[bool]: """ Generates the query result for each child. """ diff --git a/setup.cfg b/setup.cfg index bb80fda7..de9f4c37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,10 +37,15 @@ python_requires = >=3.6 packages = django_auth_ldap install_requires = Django>=2.2 - python-ldap>=3.1 + ldap3>=2.8.1 [flake8] max-line-length = 88 [isort] profile = black + +[mypy] +ignore_missing_imports = True +no_implicit_optional = True +strict_equality = True diff --git a/tests/test_server_entries.json b/tests/test_server_entries.json new file mode 100644 index 00000000..90490934 --- /dev/null +++ b/tests/test_server_entries.json @@ -0,0 +1,892 @@ +{ + "entries": [ + { + "attributes": { + "o": [ + "test" + ], + "objectClass": [ + "organization" + ] + }, + "dn": "o=test", + "raw": { + "o": [ + "test" + ], + "objectClass": [ + "organization" + ] + } + }, + { + "attributes": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "groups" + ] + }, + "dn": "ou=groups,o=test", + "raw": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "groups" + ] + } + }, + { + "attributes": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "people" + ] + }, + "dn": "ou=people,o=test", + "raw": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "people" + ] + } + }, + { + "attributes": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "moregroups" + ] + }, + "dn": "ou=moregroups,o=test", + "raw": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "moregroups" + ] + } + }, + { + "attributes": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "query_groups" + ] + }, + "dn": "ou=query_groups,o=test", + "raw": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "query_groups" + ] + } + }, + { + "attributes": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "mirror_groups" + ] + }, + "dn": "ou=mirror_groups,o=test", + "raw": { + "objectClass": [ + "organizationalUnit" + ], + "ou": [ + "mirror_groups" + ] + } + }, + { + "attributes": { + "cn": [ + "staff_px" + ], + "gidNumber": 1001, + "memberUid": [ + "alice", + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + }, + "dn": "cn=staff_px,ou=groups,o=test", + "raw": { + "cn": [ + "staff_px" + ], + "gidNumber": [ + "1001" + ], + "memberUid": [ + "alice", + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + } + }, + { + "attributes": { + "cn": [ + "active_px" + ], + "gidNumber": 1000, + "memberUid": [ + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + }, + "dn": "cn=active_px,ou=groups,o=test", + "raw": { + "cn": [ + "active_px" + ], + "gidNumber": [ + "1000" + ], + "memberUid": [ + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + } + }, + { + "attributes": { + "cn": [ + "empty_gon" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=empty_gon,ou=groups,o=test", + "raw": { + "cn": [ + "empty_gon" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "staff_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=staff_gon,ou=groups,o=test", + "raw": { + "cn": [ + "staff_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "active_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=active_gon,ou=groups,o=test", + "raw": { + "cn": [ + "active_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "nested_gon" + ], + "member": [ + "uid=alice,ou=people,o=test", + "cn=circular_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=nested_gon,ou=groups,o=test", + "raw": { + "cn": [ + "nested_gon" + ], + "member": [ + "uid=alice,ou=people,o=test", + "cn=circular_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "parent_gon" + ], + "member": [ + "cn=nested_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=parent_gon,ou=groups,o=test", + "raw": { + "cn": [ + "parent_gon" + ], + "member": [ + "cn=nested_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "circular_gon" + ], + "member": [ + "cn=parent_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=circular_gon,ou=groups,o=test", + "raw": { + "cn": [ + "circular_gon" + ], + "member": [ + "cn=parent_gon,ou=groups,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "superuser_px" + ], + "gidNumber": 1002, + "memberUid": [ + "alice", + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + }, + "dn": "cn=superuser_px,ou=groups,o=test", + "raw": { + "cn": [ + "superuser_px" + ], + "gidNumber": [ + "1002" + ], + "memberUid": [ + "alice", + "nonposix" + ], + "objectClass": [ + "posixGroup" + ] + } + }, + { + "attributes": { + "cn": [ + "superuser_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=superuser_gon,ou=groups,o=test", + "raw": { + "cn": [ + "superuser_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "bob" + ], + "gidNumber": 50, + "givenName": [ + "Robert" + ], + "homeDirectory": "/home/bob", + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Barker" + ], + "uid": [ + "bob" + ], + "uidNumber": 1001, + "userPassword": [ + "password" + ] + }, + "dn": "uid=bob,ou=people,o=test", + "raw": { + "cn": [ + "bob" + ], + "gidNumber": [ + "50" + ], + "givenName": [ + "Robert" + ], + "homeDirectory": [ + "/home/bob" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Barker" + ], + "uid": [ + "bob" + ], + "uidNumber": [ + "1001" + ], + "userPassword": [ + "password" + ] + } + }, + { + "attributes": { + "cn": [ + "alice" + ], + "gidNumber": 1000, + "givenName": [ + "Alice" + ], + "homeDirectory": "/home/alice", + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Adams" + ], + "uid": [ + "alice" + ], + "uidNumber": 1000, + "userPassword": [ + "password" + ] + }, + "dn": "uid=alice,ou=people,o=test", + "raw": { + "cn": [ + "alice" + ], + "gidNumber": [ + "1000" + ], + "givenName": [ + "Alice" + ], + "homeDirectory": [ + "/home/alice" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Adams" + ], + "uid": [ + "alice" + ], + "uidNumber": [ + "1000" + ], + "userPassword": [ + "password" + ] + } + }, + { + "attributes": { + "cn": [ + "nobody" + ], + "gidNumber": 50, + "homeDirectory": "/home/nobody", + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "nobody" + ], + "uid": [ + "nobody" + ], + "uidNumber": 1003, + "userPassword": [ + "password" + ] + }, + "dn": "uid=nobody,ou=people,o=test", + "raw": { + "cn": [ + "nobody" + ], + "gidNumber": [ + "50" + ], + "homeDirectory": [ + "/home/nobody" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "nobody" + ], + "uid": [ + "nobody" + ], + "uidNumber": [ + "1003" + ], + "userPassword": [ + "password" + ] + } + }, + { + "attributes": { + "cn": [ + "dre\u00dfler" + ], + "gidNumber": 50, + "givenName": [ + "Wolfgang" + ], + "homeDirectory": "/home/dressler", + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Dre\u00dfler" + ], + "uid": [ + "dre\u00dfler" + ], + "uidNumber": 1002, + "userPassword": [ + "password" + ] + }, + "dn": "uid=dre\u00dfler,ou=people,o=test", + "raw": { + "cn": [ + "dre\u00dfler" + ], + "gidNumber": [ + "50" + ], + "givenName": [ + "Wolfgang" + ], + "homeDirectory": [ + "/home/dressler" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson", + "posixAccount" + ], + "sn": [ + "Dre\u00dfler" + ], + "uid": [ + "dre\u00dfler" + ], + "uidNumber": [ + "1002" + ], + "userPassword": [ + "password" + ] + } + }, + { + "attributes": { + "cn": [ + "nonposix" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson" + ], + "sn": [ + "nonposix" + ], + "uid": [ + "nonposix" + ], + "userPassword": [ + "password" + ] + }, + "dn": "uid=nonposix,ou=people,o=test", + "raw": { + "cn": [ + "nonposix" + ], + "objectClass": [ + "person", + "organizationalPerson", + "inetOrgPerson" + ], + "sn": [ + "nonposix" + ], + "uid": [ + "nonposix" + ], + "userPassword": [ + "password" + ] + } + }, + { + "attributes": { + "cn": [ + "other_gon" + ], + "member": [ + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=other_gon,ou=moregroups,o=test", + "raw": { + "cn": [ + "other_gon" + ], + "member": [ + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "bob_gon" + ], + "member": [ + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=bob_gon,ou=query_groups,o=test", + "raw": { + "cn": [ + "bob_gon" + ], + "member": [ + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "alice_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=alice_gon,ou=query_groups,o=test", + "raw": { + "cn": [ + "alice_gon" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "mutual_gon" + ], + "member": [ + "uid=alice,ou=people,o=test", + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=mutual_gon,ou=query_groups,o=test", + "raw": { + "cn": [ + "mutual_gon" + ], + "member": [ + "uid=alice,ou=people,o=test", + "uid=bob,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "dre\u00dfler_gon" + ], + "member": [ + "uid=dre\u00dfler,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=dre\u00dfler_gon,ou=query_groups,o=test", + "raw": { + "cn": [ + "dre\u00dfler_gon" + ], + "member": [ + "uid=dre\u00dfler,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "mirror1" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=mirror1,ou=mirror_groups,o=test", + "raw": { + "cn": [ + "mirror1" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "mirror2" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=mirror2,ou=mirror_groups,o=test", + "raw": { + "cn": [ + "mirror2" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "mirror3" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=mirror3,ou=mirror_groups,o=test", + "raw": { + "cn": [ + "mirror3" + ], + "member": [ + "uid=alice,ou=people,o=test" + ], + "objectClass": [ + "groupOfNames" + ] + } + }, + { + "attributes": { + "cn": [ + "mirror4" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + }, + "dn": "cn=mirror4,ou=mirror_groups,o=test", + "raw": { + "cn": [ + "mirror4" + ], + "member": [ + "" + ], + "objectClass": [ + "groupOfNames" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/test_server_info.json b/tests/test_server_info.json new file mode 100644 index 00000000..8181f413 --- /dev/null +++ b/tests/test_server_info.json @@ -0,0 +1,60 @@ +{ + "raw": { + "altServer": [], + "configContext": [ + "cn=config" + ], + "entryDN": [ + "" + ], + "namingContexts": [ + "o=test" + ], + "objectClass": [ + "top", + "OpenLDAProotDSE" + ], + "structuralObjectClass": [ + "OpenLDAProotDSE" + ], + "subschemaSubentry": [ + "cn=Subschema" + ], + "supportedCapabilities": [], + "supportedControl": [ + "2.16.840.1.113730.3.4.18", + "2.16.840.1.113730.3.4.2", + "1.3.6.1.4.1.4203.1.10.1", + "1.3.6.1.1.22", + "1.2.840.113556.1.4.319", + "1.2.826.0.1.3344810.2.3", + "1.3.6.1.1.13.2", + "1.3.6.1.1.13.1", + "1.3.6.1.1.12" + ], + "supportedExtension": [ + "1.3.6.1.4.1.4203.1.11.1", + "1.3.6.1.4.1.4203.1.11.3", + "1.3.6.1.1.8" + ], + "supportedFeatures": [ + "1.3.6.1.1.14", + "1.3.6.1.4.1.4203.1.5.1", + "1.3.6.1.4.1.4203.1.5.2", + "1.3.6.1.4.1.4203.1.5.3", + "1.3.6.1.4.1.4203.1.5.4", + "1.3.6.1.4.1.4203.1.5.5" + ], + "supportedLDAPVersion": [ + "3" + ], + "supportedSASLMechanisms": [ + "DIGEST-MD5", + "NTLM", + "CRAM-MD5" + ], + "vendorName": [], + "vendorVersion": [] + }, + "type": "DsaInfo" +} \ No newline at end of file diff --git a/tests/test_server_schema.json b/tests/test_server_schema.json new file mode 100644 index 00000000..58d2788a --- /dev/null +++ b/tests/test_server_schema.json @@ -0,0 +1,486 @@ +{ + "raw": { + "attributeTypes": [ + "( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", + "( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.3 NAME 'creatorsName' DESC 'RFC4512: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.4 NAME 'modifiersName' DESC 'RFC4512: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC4512: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 1.3.6.1.1.20 NAME 'entryDN' DESC 'DN of the entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 1.3.6.1.1.16.4 NAME 'entryUUID' DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", + "( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC4512: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC4512: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC4512: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC4512: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC4512: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC4512: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )", + "( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'RFC4512: features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", + "( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", + "( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", + "( 2.5.21.4 NAME 'matchingRules' DESC 'RFC4512: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )", + "( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )", + "( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )", + "( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC4512: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )", + "( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )", + "( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC4512: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", + "( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'RFC3296: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation )", + "( 1.3.6.1.4.1.1466.101.119.3 NAME 'entryTtl' DESC 'RFC2589: entry time-to-live' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", + "( 1.3.6.1.4.1.1466.101.119.4 NAME 'dynamicSubtrees' DESC 'RFC2589: dynamic subtrees' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation )", + "( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", + "( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name )", + "( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'RFC2307: An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'RFC2307: An integer uniquely identifying a group in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 2.5.4.35 NAME 'userPassword' DESC 'RFC4519/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )", + "( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.5.4.13 NAME 'description' DESC 'RFC4519: descriptive information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )", + "( 2.5.4.34 NAME 'seeAlso' DESC 'RFC4519: DN of related object' SUP distinguishedName )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.78 NAME 'olcConfigFile' DESC 'File for slapd configuration directives' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.79 NAME 'olcConfigDir' DESC 'Directory for slapd configuration backend' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.1 NAME 'olcAccess' DESC 'Access Control List' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.86 NAME 'olcAddContentAcl' DESC 'Check ACLs against content of Add ops' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.2 NAME 'olcAllows' DESC 'Allowed set of deprecated features' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.3 NAME 'olcArgsFile' DESC 'File for slapd command line options' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.5 NAME 'olcAttributeOptions' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.4 NAME 'olcAttributeTypes' DESC 'OpenLDAP attributeTypes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.6 NAME 'olcAuthIDRewrite' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.7 NAME 'olcAuthzPolicy' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.8 NAME 'olcAuthzRegexp' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.9 NAME 'olcBackend' DESC 'A type of backend' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORDERED 'SIBLINGS' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.10 NAME 'olcConcurrency' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.11 NAME 'olcConnMaxPending' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.12 NAME 'olcConnMaxPendingAuth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.13 NAME 'olcDatabase' DESC 'The backend type for a database instance' SUP olcBackend SINGLE-VALUE X-ORDERED 'SIBLINGS' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.14 NAME 'olcDefaultSearchBase' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.15 NAME 'olcDisallows' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.16 NAME 'olcDitContentRules' DESC 'OpenLDAP DIT content rules' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.20 NAME 'olcExtraAttrs' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.17 NAME 'olcGentleHUP' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.17 NAME 'olcHidden' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.18 NAME 'olcIdleTimeout' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.19 NAME 'olcInclude' SUP labeledURI )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.20 NAME 'olcIndexSubstrIfMinLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.21 NAME 'olcIndexSubstrIfMaxLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.22 NAME 'olcIndexSubstrAnyLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.23 NAME 'olcIndexSubstrAnyStep' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.84 NAME 'olcIndexIntLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.4 NAME 'olcLastMod' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.85 NAME 'olcLdapSyntaxes' DESC 'OpenLDAP ldapSyntax' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.5 NAME 'olcLimits' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.93 NAME 'olcListenerThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.26 NAME 'olcLocalSSF' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.27 NAME 'olcLogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.28 NAME 'olcLogLevel' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.6 NAME 'olcMaxDerefDepth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.16 NAME 'olcMirrorMode' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.30 NAME 'olcModuleLoad' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.31 NAME 'olcModulePath' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.18 NAME 'olcMonitoring' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.32 NAME 'olcObjectClasses' DESC 'OpenLDAP object classes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.33 NAME 'olcObjectIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.34 NAME 'olcOverlay' SUP olcDatabase SINGLE-VALUE X-ORDERED 'SIBLINGS' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.35 NAME 'olcPasswordCryptSaltFormat' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.36 NAME 'olcPasswordHash' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.37 NAME 'olcPidFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.38 NAME 'olcPlugin' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.39 NAME 'olcPluginLogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.40 NAME 'olcReadOnly' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.41 NAME 'olcReferral' SUP labeledURI SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.7 NAME 'olcReplica' SUP labeledURI EQUALITY caseIgnoreMatch X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.43 NAME 'olcReplicaArgsFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.44 NAME 'olcReplicaPidFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.45 NAME 'olcReplicationInterval' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.46 NAME 'olcReplogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.47 NAME 'olcRequires' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.48 NAME 'olcRestrict' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.49 NAME 'olcReverseLookup' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.8 NAME 'olcRootDN' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.51 NAME 'olcRootDSE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.9 NAME 'olcRootPW' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.89 NAME 'olcSaslAuxprops' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.53 NAME 'olcSaslHost' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.54 NAME 'olcSaslRealm' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.56 NAME 'olcSaslSecProps' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.58 NAME 'olcSchemaDN' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.59 NAME 'olcSecurity' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.81 NAME 'olcServerID' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.60 NAME 'olcSizeLimit' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.61 NAME 'olcSockbufMaxIncoming' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.62 NAME 'olcSockbufMaxIncomingAuth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.83 NAME 'olcSortVals' DESC 'Attributes whose values will always be sorted' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.15 NAME 'olcSubordinate' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.10 NAME 'olcSuffix' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.19 NAME 'olcSyncUseSubentry' DESC 'Store sync context in a subentry' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.11 NAME 'olcSyncrepl' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.90 NAME 'olcTCPBuffer' DESC 'Custom TCP buffer size' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.66 NAME 'olcThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.67 NAME 'olcTimeLimit' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.68 NAME 'olcTLSCACertificateFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.69 NAME 'olcTLSCACertificatePath' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.70 NAME 'olcTLSCertificateFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.71 NAME 'olcTLSCertificateKeyFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.72 NAME 'olcTLSCipherSuite' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.73 NAME 'olcTLSCRLCheck' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.82 NAME 'olcTLSCRLFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.74 NAME 'olcTLSRandFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.75 NAME 'olcTLSVerifyClient' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.77 NAME 'olcTLSDHParamFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.87 NAME 'olcTLSProtocolMin' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.80 NAME 'olcToolThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.12 NAME 'olcUpdateDN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.13 NAME 'olcUpdateRef' SUP labeledURI EQUALITY caseIgnoreMatch )", + "( 1.3.6.1.4.1.4203.1.12.2.3.0.88 NAME 'olcWriteTimeout' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.1 NAME 'olcDbDirectory' DESC 'Directory for database content' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.2 NAME 'olcDbCheckpoint' DESC 'Database checkpoint interval in kbytes and minutes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.4 NAME 'olcDbNoSync' DESC 'Disable synchronous database writes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.3 NAME 'olcDbEnvFlags' DESC 'Database environment flags' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.2 NAME 'olcDbIndex' DESC 'Attribute index parameters' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.1 NAME 'olcDbMaxReaders' DESC 'Maximum number of threads that may access the DB concurrently' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.2 NAME 'olcDbMaxSize' DESC 'Maximum size of DB in bytes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.3 NAME 'olcDbMode' DESC 'Unix permissions of database files' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.5 NAME 'olcDbRtxnSize' DESC 'Number of entries to process in one read transaction' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.9 NAME 'olcDbSearchStack' DESC 'Depth of search stack in IDLs' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 2.5.4.2 NAME 'knowledgeInformation' DESC 'RFC2256: knowledge information' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", + "( 2.5.4.4 NAME ( 'sn' 'surname' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name )", + "( 2.5.4.5 NAME 'serialNumber' DESC 'RFC2256: serial number of the entity' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} )", + "( 2.5.4.6 NAME ( 'c' 'countryName' ) DESC 'RFC4519: two-letter ISO-3166 country code' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE )", + "( 2.5.4.7 NAME ( 'l' 'localityName' ) DESC 'RFC2256: locality which this object resides in' SUP name )", + "( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' ) DESC 'RFC2256: state or province which this object resides in' SUP name )", + "( 2.5.4.9 NAME ( 'street' 'streetAddress' ) DESC 'RFC2256: street address of this object' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", + "( 2.5.4.10 NAME ( 'o' 'organizationName' ) DESC 'RFC2256: organization this object belongs to' SUP name )", + "( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )", + "( 2.5.4.12 NAME 'title' DESC 'RFC2256: title associated with the entity' SUP name )", + "( 2.5.4.14 NAME 'searchGuide' DESC 'RFC2256: search guide, deprecated by enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 )", + "( 2.5.4.15 NAME 'businessCategory' DESC 'RFC2256: business category' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", + "( 2.5.4.16 NAME 'postalAddress' DESC 'RFC2256: postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", + "( 2.5.4.17 NAME 'postalCode' DESC 'RFC2256: postal code' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )", + "( 2.5.4.18 NAME 'postOfficeBox' DESC 'RFC2256: Post Office Box' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )", + "( 2.5.4.19 NAME 'physicalDeliveryOfficeName' DESC 'RFC2256: Physical Delivery Office Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", + "( 2.5.4.20 NAME 'telephoneNumber' DESC 'RFC2256: Telephone Number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} )", + "( 2.5.4.21 NAME 'telexNumber' DESC 'RFC2256: Telex Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )", + "( 2.5.4.22 NAME 'teletexTerminalIdentifier' DESC 'RFC2256: Teletex Terminal Identifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )", + "( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' ) DESC 'RFC2256: Facsimile (Fax) Telephone Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )", + "( 2.5.4.24 NAME 'x121Address' DESC 'RFC2256: X.121 Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{15} )", + "( 2.5.4.25 NAME 'internationaliSDNNumber' DESC 'RFC2256: international ISDN number' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} )", + "( 2.5.4.26 NAME 'registeredAddress' DESC 'RFC2256: registered postal address' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", + "( 2.5.4.27 NAME 'destinationIndicator' DESC 'RFC2256: destination indicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} )", + "( 2.5.4.28 NAME 'preferredDeliveryMethod' DESC 'RFC2256: preferred delivery method' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE )", + "( 2.5.4.29 NAME 'presentationAddress' DESC 'RFC2256: presentation address' EQUALITY presentationAddressMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.43 SINGLE-VALUE )", + "( 2.5.4.30 NAME 'supportedApplicationContext' DESC 'RFC2256: supported application context' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", + "( 2.5.4.31 NAME 'member' DESC 'RFC2256: member of a group' SUP distinguishedName )", + "( 2.5.4.32 NAME 'owner' DESC 'RFC2256: owner (of the object)' SUP distinguishedName )", + "( 2.5.4.33 NAME 'roleOccupant' DESC 'RFC2256: occupant of role' SUP distinguishedName )", + "( 2.5.4.36 NAME 'userCertificate' DESC 'RFC2256: X.509 user certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )", + "( 2.5.4.37 NAME 'cACertificate' DESC 'RFC2256: X.509 CA certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )", + "( 2.5.4.38 NAME 'authorityRevocationList' DESC 'RFC2256: X.509 authority revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", + "( 2.5.4.39 NAME 'certificateRevocationList' DESC 'RFC2256: X.509 certificate revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", + "( 2.5.4.40 NAME 'crossCertificatePair' DESC 'RFC2256: X.509 cross certificate pair, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 )", + "( 2.5.4.42 NAME ( 'givenName' 'gn' ) DESC 'RFC2256: first name(s) for which the entity is known by' SUP name )", + "( 2.5.4.43 NAME 'initials' DESC 'RFC2256: initials of some or all of names, but not the surname(s).' SUP name )", + "( 2.5.4.44 NAME 'generationQualifier' DESC 'RFC2256: name qualifier indicating a generation' SUP name )", + "( 2.5.4.45 NAME 'x500UniqueIdentifier' DESC 'RFC2256: X.500 unique identifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", + "( 2.5.4.46 NAME 'dnQualifier' DESC 'RFC2256: DN qualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )", + "( 2.5.4.47 NAME 'enhancedSearchGuide' DESC 'RFC2256: enhanced search guide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )", + "( 2.5.4.48 NAME 'protocolInformation' DESC 'RFC2256: protocol information' EQUALITY protocolInformationMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.42 )", + "( 2.5.4.50 NAME 'uniqueMember' DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", + "( 2.5.4.51 NAME 'houseIdentifier' DESC 'RFC2256: house identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", + "( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'RFC2256: supported algorithms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 )", + "( 2.5.4.53 NAME 'deltaRevocationList' DESC 'RFC2256: delta revocation list; use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", + "( 2.5.4.54 NAME 'dmdName' DESC 'RFC2256: name of DMD' SUP name )", + "( 2.5.4.65 NAME 'pseudonym' DESC 'X.520(4th): pseudonym for the object' SUP name )", + "( 0.9.2342.19200300.100.1.3 NAME ( 'mail' 'rfc822Mailbox' ) DESC 'RFC1274: RFC822 Mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", + "( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainComponent' ) DESC 'RFC1274/2247: domain component' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", + "( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' DESC 'RFC1274: domain associated with object' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.2.840.113549.1.9.1 NAME ( 'email' 'emailAddress' 'pkcs9email' ) DESC 'RFC3280: legacy attribute for email addresses in DNs' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )", + "( 0.9.2342.19200300.100.1.2 NAME 'textEncodedORAddress' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.4 NAME 'info' DESC 'RFC1274: general information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} )", + "( 0.9.2342.19200300.100.1.5 NAME ( 'drink' 'favouriteDrink' ) DESC 'RFC1274: favorite drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' DESC 'RFC1274: room number' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.7 NAME 'photo' DESC 'RFC1274: photo (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23{25000} )", + "( 0.9.2342.19200300.100.1.8 NAME 'userClass' DESC 'RFC1274: category of user' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.9 NAME 'host' DESC 'RFC1274: host computer' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.10 NAME 'manager' DESC 'RFC1274: DN of manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' DESC 'RFC1274: unique identifier of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' DESC 'RFC1274: title of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' DESC 'RFC1274: version of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' DESC 'RFC1274: DN of author of document' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' DESC 'RFC1274: location of document original' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.20 NAME ( 'homePhone' 'homeTelephoneNumber' ) DESC 'RFC1274: home telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", + "( 0.9.2342.19200300.100.1.21 NAME 'secretary' DESC 'RFC1274: DN of secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 0.9.2342.19200300.100.1.22 NAME 'otherMailbox' SYNTAX 1.3.6.1.4.1.1466.115.121.1.39 )", + "( 0.9.2342.19200300.100.1.26 NAME 'aRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.27 NAME 'mDRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.28 NAME 'mXRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.29 NAME 'nSRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.30 NAME 'sOARecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.31 NAME 'cNAMERecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 0.9.2342.19200300.100.1.38 NAME 'associatedName' DESC 'RFC1274: DN of entry associated with domain' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' DESC 'RFC1274: home postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", + "( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' DESC 'RFC1274: personal title' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.41 NAME ( 'mobile' 'mobileTelephoneNumber' ) DESC 'RFC1274: mobile telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", + "( 0.9.2342.19200300.100.1.42 NAME ( 'pager' 'pagerTelephoneNumber' ) DESC 'RFC1274: pager telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", + "( 0.9.2342.19200300.100.1.43 NAME ( 'co' 'friendlyCountryName' ) DESC 'RFC1274: friendly country name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' DESC 'RFC1274: unique identifer' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' DESC 'RFC1274: organizational status' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.46 NAME 'janetMailbox' DESC 'RFC1274: Janet mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", + "( 0.9.2342.19200300.100.1.47 NAME 'mailPreferenceOption' DESC 'RFC1274: mail preference option' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 0.9.2342.19200300.100.1.48 NAME 'buildingName' DESC 'RFC1274: name of building' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", + "( 0.9.2342.19200300.100.1.49 NAME 'dSAQuality' DESC 'RFC1274: DSA Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.19 SINGLE-VALUE )", + "( 0.9.2342.19200300.100.1.50 NAME 'singleLevelQuality' DESC 'RFC1274: Single Level Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", + "( 0.9.2342.19200300.100.1.51 NAME 'subtreeMinimumQuality' DESC 'RFC1274: Subtree Mininum Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", + "( 0.9.2342.19200300.100.1.52 NAME 'subtreeMaximumQuality' DESC 'RFC1274: Subtree Maximun Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", + "( 0.9.2342.19200300.100.1.53 NAME 'personalSignature' DESC 'RFC1274: Personal Signature (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23 )", + "( 0.9.2342.19200300.100.1.54 NAME 'dITRedirect' DESC 'RFC1274: DIT Redirect' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 0.9.2342.19200300.100.1.55 NAME 'audio' DESC 'RFC1274: audio (u-law)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.4{25000} )", + "( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' DESC 'RFC1274: publisher of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' SYNTAX 1.3.6.1.1.1.0.0 )", + "( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' SUP name )", + "( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IP address' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )", + "( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE )", + "( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )", + "( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' SYNTAX 1.3.6.1.1.1.0.1 )", + "( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.1.1.1.26 NAME 'nisMapName' SUP name )", + "( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{1024} SINGLE-VALUE )", + "( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'RFC2798: vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC 'RFC2798: identifies a department within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'RFC2798: preferred name to be used when displaying entries' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'RFC2798: numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'RFC2798: type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'RFC2798: a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )", + "( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DESC 'RFC2798: preferred written or spoken language for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", + "( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate' DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )", + "( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'RFC2798: personal identity information, a PKCS #12 PFX' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )" + ], + "cn": [ + "Subschema" + ], + "createTimestamp": [ + "20200912032156Z" + ], + "dITContentRules": [], + "dITStructureRules": [], + "entryDN": [ + "cn=Subschema" + ], + "ldapSyntaxes": [ + "( 1.3.6.1.4.1.1466.115.121.1.4 DESC 'Audio' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.5 DESC 'Binary' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.6 DESC 'Bit String' )", + "( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' )", + "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.9 DESC 'Certificate List' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.10 DESC 'Certificate Pair' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.4203.666.11.10.2.1 DESC 'X.509 AttributeCertificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )", + "( 1.2.36.79672281.1.5.0 DESC 'RDN' )", + "( 1.3.6.1.4.1.1466.115.121.1.14 DESC 'Delivery Method' )", + "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )", + "( 1.3.6.1.4.1.1466.115.121.1.22 DESC 'Facsimile Telephone Number' )", + "( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )", + "( 1.3.6.1.4.1.1466.115.121.1.26 DESC 'IA5 String' )", + "( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )", + "( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )", + "( 1.3.6.1.4.1.1466.115.121.1.36 DESC 'Numeric String' )", + "( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )", + "( 1.3.6.1.4.1.1466.115.121.1.39 DESC 'Other Mailbox' )", + "( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' )", + "( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' )", + "( 1.3.6.1.4.1.1466.115.121.1.44 DESC 'Printable String' )", + "( 1.3.6.1.4.1.1466.115.121.1.11 DESC 'Country String' )", + "( 1.3.6.1.4.1.1466.115.121.1.45 DESC 'SubtreeSpecification' )", + "( 1.3.6.1.4.1.1466.115.121.1.49 DESC 'Supported Algorithm' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", + "( 1.3.6.1.4.1.1466.115.121.1.50 DESC 'Telephone Number' )", + "( 1.3.6.1.4.1.1466.115.121.1.52 DESC 'Telex Number' )", + "( 1.3.6.1.1.1.0.0 DESC 'RFC2307 NIS Netgroup Triple' )", + "( 1.3.6.1.1.1.0.1 DESC 'RFC2307 Boot Parameter' )", + "( 1.3.6.1.1.16.1 DESC 'UUID' )" + ], + "matchingRuleUse": [ + "( 1.2.840.113556.1.4.804 NAME 'integerBitOrMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", + "( 1.2.840.113556.1.4.803 NAME 'integerBitAndMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", + "( 1.3.6.1.4.1.1466.109.114.2 NAME 'caseIgnoreIA5Match' APPLIES ( altServer $ c $ mail $ dc $ associatedDomain $ email $ aRecord $ mDRecord $ mXRecord $ nSRecord $ sOARecord $ cNAMERecord $ janetMailbox $ gecos $ homeDirectory $ loginShell $ memberUid $ memberNisNetgroup $ ipHostNumber $ ipNetworkNumber $ ipNetmaskNumber $ macAddress $ bootFile $ nisMapEntry ) )", + "( 1.3.6.1.4.1.1466.109.114.1 NAME 'caseExactIA5Match' APPLIES ( altServer $ c $ mail $ dc $ associatedDomain $ email $ aRecord $ mDRecord $ mXRecord $ nSRecord $ sOARecord $ cNAMERecord $ janetMailbox $ gecos $ homeDirectory $ loginShell $ memberUid $ memberNisNetgroup $ ipHostNumber $ ipNetworkNumber $ ipNetmaskNumber $ macAddress $ bootFile $ nisMapEntry ) )", + "( 2.5.13.38 NAME 'certificateListExactMatch' APPLIES ( authorityRevocationList $ certificateRevocationList $ deltaRevocationList ) )", + "( 2.5.13.34 NAME 'certificateExactMatch' APPLIES ( userCertificate $ cACertificate ) )", + "( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' APPLIES ( supportedControl $ supportedExtension $ supportedFeatures $ ldapSyntaxes $ supportedApplicationContext ) )", + "( 2.5.13.29 NAME 'integerFirstComponentMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", + "( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' APPLIES ( createTimestamp $ modifyTimestamp ) )", + "( 2.5.13.27 NAME 'generalizedTimeMatch' APPLIES ( createTimestamp $ modifyTimestamp ) )", + "( 2.5.13.24 NAME 'protocolInformationMatch' APPLIES protocolInformation )", + "( 2.5.13.23 NAME 'uniqueMemberMatch' APPLIES uniqueMember )", + "( 2.5.13.22 NAME 'presentationAddressMatch' APPLIES presentationAddress )", + "( 2.5.13.20 NAME 'telephoneNumberMatch' APPLIES ( telephoneNumber $ homePhone $ mobile $ pager ) )", + "( 2.5.13.18 NAME 'octetStringOrderingMatch' APPLIES userPassword )", + "( 2.5.13.17 NAME 'octetStringMatch' APPLIES userPassword )", + "( 2.5.13.16 NAME 'bitStringMatch' APPLIES x500UniqueIdentifier )", + "( 2.5.13.15 NAME 'integerOrderingMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", + "( 2.5.13.14 NAME 'integerMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", + "( 2.5.13.13 NAME 'booleanMatch' APPLIES ( hasSubordinates $ olcAddContentAcl $ olcGentleHUP $ olcHidden $ olcLastMod $ olcMirrorMode $ olcMonitoring $ olcReadOnly $ olcReverseLookup $ olcSyncUseSubentry $ olcDbNoSync ) )", + "( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )", + "( 2.5.13.9 NAME 'numericStringOrderingMatch' APPLIES ( x121Address $ internationaliSDNNumber ) )", + "( 2.5.13.8 NAME 'numericStringMatch' APPLIES ( x121Address $ internationaliSDNNumber ) )", + "( 2.5.13.7 NAME 'caseExactSubstringsMatch' APPLIES ( serialNumber $ c $ telephoneNumber $ destinationIndicator $ dnQualifier $ homePhone $ mobile $ pager ) )", + "( 2.5.13.6 NAME 'caseExactOrderingMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", + "( 2.5.13.5 NAME 'caseExactMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", + "( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' APPLIES ( serialNumber $ c $ telephoneNumber $ destinationIndicator $ dnQualifier $ homePhone $ mobile $ pager ) )", + "( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", + "( 2.5.13.2 NAME 'caseIgnoreMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", + "( 2.5.13.1 NAME 'distinguishedNameMatch' APPLIES ( creatorsName $ modifiersName $ subschemaSubentry $ entryDN $ namingContexts $ aliasedObjectName $ dynamicSubtrees $ distinguishedName $ seeAlso $ olcDefaultSearchBase $ olcRootDN $ olcSchemaDN $ olcSuffix $ olcUpdateDN $ member $ owner $ roleOccupant $ manager $ documentAuthor $ secretary $ associatedName $ dITRedirect ) )", + "( 2.5.13.0 NAME 'objectIdentifierMatch' APPLIES ( supportedControl $ supportedExtension $ supportedFeatures $ supportedApplicationContext ) )" + ], + "matchingRules": [ + "( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )", + "( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )", + "( 1.2.840.113556.1.4.804 NAME 'integerBitOrMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 1.2.840.113556.1.4.803 NAME 'integerBitAndMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 1.3.6.1.4.1.4203.1.2.1 NAME 'caseExactIA5SubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.4.1.1466.109.114.3 NAME 'caseIgnoreIA5SubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.4.1.1466.109.114.2 NAME 'caseIgnoreIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 1.3.6.1.4.1.1466.109.114.1 NAME 'caseExactIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", + "( 2.5.13.38 NAME 'certificateListExactMatch' SYNTAX 1.3.6.1.1.15.5 )", + "( 2.5.13.34 NAME 'certificateExactMatch' SYNTAX 1.3.6.1.1.15.1 )", + "( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", + "( 2.5.13.29 NAME 'integerFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", + "( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", + "( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", + "( 2.5.13.21 NAME 'telephoneNumberSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", + "( 2.5.13.20 NAME 'telephoneNumberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", + "( 2.5.13.19 NAME 'octetStringSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", + "( 2.5.13.18 NAME 'octetStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", + "( 2.5.13.17 NAME 'octetStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", + "( 2.5.13.16 NAME 'bitStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", + "( 2.5.13.15 NAME 'integerOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 2.5.13.14 NAME 'integerMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", + "( 2.5.13.13 NAME 'booleanMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )", + "( 2.5.13.11 NAME 'caseIgnoreListMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", + "( 2.5.13.10 NAME 'numericStringSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", + "( 2.5.13.9 NAME 'numericStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", + "( 2.5.13.8 NAME 'numericStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", + "( 2.5.13.7 NAME 'caseExactSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", + "( 2.5.13.6 NAME 'caseExactOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.5.13.5 NAME 'caseExactMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", + "( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", + "( 1.2.36.79672281.1.13.3 NAME 'rdnMatch' SYNTAX 1.2.36.79672281.1.5.0 )", + "( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", + "( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )" + ], + "modifyTimestamp": [ + "20200912032156Z" + ], + "nameForms": [], + "objectClass": [ + "top", + "subentry", + "subschema", + "extensibleObject" + ], + "objectClasses": [ + "( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )", + "( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC4512: extensible object' SUP top AUXILIARY )", + "( 2.5.6.1 NAME 'alias' DESC 'RFC4512: an alias' SUP top STRUCTURAL MUST aliasedObjectName )", + "( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref )", + "( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )", + "( 2.5.17.0 NAME 'subentry' DESC 'RFC3672: subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )", + "( 2.5.20.1 NAME 'subschema' DESC 'RFC4512: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ dITContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )", + "( 1.3.6.1.4.1.1466.101.119.2 NAME 'dynamicObject' DESC 'RFC2589: Dynamic Object' SUP top AUXILIARY )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.0 NAME 'olcConfig' DESC 'OpenLDAP configuration object' SUP top ABSTRACT )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.1 NAME 'olcGlobal' DESC 'OpenLDAP Global configuration options' SUP olcConfig STRUCTURAL MAY ( cn $ olcConfigFile $ olcConfigDir $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcDisallows $ olcGentleHUP $ olcIdleTimeout $ olcIndexSubstrIfMaxLen $ olcIndexSubstrIfMinLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcLogFile $ olcLogLevel $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPluginLogFile $ olcReadOnly $ olcReferral $ olcReplogFile $ olcRequires $ olcRestrict $ olcReverseLookup $ olcRootDSE $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcTCPBuffer $ olcThreads $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSCRLFile $ olcTLSProtocolMin $ olcToolThreads $ olcWriteTimeout $ olcObjectIdentifier $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules $ olcLdapSyntaxes ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.2 NAME 'olcSchemaConfig' DESC 'OpenLDAP schema object' SUP olcConfig STRUCTURAL MAY ( cn $ olcObjectIdentifier $ olcLdapSyntaxes $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.3 NAME 'olcBackendConfig' DESC 'OpenLDAP Backend-specific options' SUP olcConfig STRUCTURAL MUST olcBackend )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.4 NAME 'olcDatabaseConfig' DESC 'OpenLDAP Database-specific options' SUP olcConfig STRUCTURAL MUST olcDatabase MAY ( olcHidden $ olcSuffix $ olcSubordinate $ olcAccess $ olcAddContentAcl $ olcLastMod $ olcLimits $ olcMaxDerefDepth $ olcPlugin $ olcReadOnly $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplicationInterval $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDN $ olcRootPW $ olcSchemaDN $ olcSecurity $ olcSizeLimit $ olcSyncUseSubentry $ olcSyncrepl $ olcTimeLimit $ olcUpdateDN $ olcUpdateRef $ olcMirrorMode $ olcMonitoring $ olcExtraAttrs ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.5 NAME 'olcOverlayConfig' DESC 'OpenLDAP Overlay-specific options' SUP olcConfig STRUCTURAL MUST olcOverlay )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.6 NAME 'olcIncludeFile' DESC 'OpenLDAP configuration include file' SUP olcConfig STRUCTURAL MUST olcInclude MAY ( cn $ olcRootDSE ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.7 NAME 'olcFrontendConfig' DESC 'OpenLDAP frontend configuration' AUXILIARY MAY ( olcDefaultSearchBase $ olcPasswordHash $ olcSortVals ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.0.8 NAME 'olcModuleList' DESC 'OpenLDAP dynamic module info' SUP olcConfig STRUCTURAL MAY ( cn $ olcModulePath $ olcModuleLoad ) )", + "( 1.3.6.1.4.1.4203.1.12.2.4.2.2.1 NAME 'olcLdifConfig' DESC 'LDIF backend configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory )", + "( 1.3.6.1.4.1.4203.1.12.2.4.2.12.1 NAME 'olcMdbConfig' DESC 'MDB backend configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory MAY ( olcDbCheckpoint $ olcDbEnvFlags $ olcDbNoSync $ olcDbIndex $ olcDbMaxReaders $ olcDbMaxSize $ olcDbMode $ olcDbSearchStack $ olcDbRtxnSize ) )", + "( 2.5.6.2 NAME 'country' DESC 'RFC2256: a country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) )", + "( 2.5.6.3 NAME 'locality' DESC 'RFC2256: a locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) )", + "( 2.5.6.4 NAME 'organization' DESC 'RFC2256: an organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", + "( 2.5.6.5 NAME 'organizationalUnit' DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", + "( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )", + "( 2.5.6.7 NAME 'organizationalPerson' DESC 'RFC2256: an organizational person' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )", + "( 2.5.6.8 NAME 'organizationalRole' DESC 'RFC2256: an organizational role' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) )", + "( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256: a group of names (DNs)' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", + "( 2.5.6.10 NAME 'residentialPerson' DESC 'RFC2256: an residential person' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) )", + "( 2.5.6.11 NAME 'applicationProcess' DESC 'RFC2256: an application process' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) )", + "( 2.5.6.12 NAME 'applicationEntity' DESC 'RFC2256: an application entity' SUP top STRUCTURAL MUST ( presentationAddress $ cn ) MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ description ) )", + "( 2.5.6.13 NAME 'dSA' DESC 'RFC2256: a directory system agent (a server)' SUP applicationEntity STRUCTURAL MAY knowledgeInformation )", + "( 2.5.6.14 NAME 'device' DESC 'RFC2256: a device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )", + "( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'RFC2256: a strong authentication user' SUP top AUXILIARY MUST userCertificate )", + "( 2.5.6.16 NAME 'certificationAuthority' DESC 'RFC2256: a certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair )", + "( 2.5.6.17 NAME 'groupOfUniqueNames' DESC 'RFC2256: a group of unique names (DN and Unique Identifier)' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", + "( 2.5.6.18 NAME 'userSecurityInformation' DESC 'RFC2256: a user security information' SUP top AUXILIARY MAY supportedAlgorithms )", + "( 2.5.6.16.2 NAME 'certificationAuthority-V2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList )", + "( 2.5.6.19 NAME 'cRLDistributionPoint' SUP top STRUCTURAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) )", + "( 2.5.6.20 NAME 'dmd' SUP top STRUCTURAL MUST dmdName MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", + "( 2.5.6.21 NAME 'pkiUser' DESC 'RFC2587: a PKI user' SUP top AUXILIARY MAY userCertificate )", + "( 2.5.6.22 NAME 'pkiCA' DESC 'RFC2587: PKI certificate authority' SUP top AUXILIARY MAY ( authorityRevocationList $ certificateRevocationList $ cACertificate $ crossCertificatePair ) )", + "( 2.5.6.23 NAME 'deltaCRL' DESC 'RFC2587: PKI user' SUP top AUXILIARY MAY deltaRevocationList )", + "( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'RFC2079: object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI )", + "( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPassword )", + "( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247: domain component object' SUP top AUXILIARY MUST dc )", + "( 1.3.6.1.1.3.1 NAME 'uidObject' DESC 'RFC2377: uid object' SUP top AUXILIARY MUST uid )", + "( 0.9.2342.19200300.100.4.4 NAME ( 'pilotPerson' 'newPilotPerson' ) SUP person STRUCTURAL MAY ( userid $ textEncodedORAddress $ rfc822Mailbox $ favouriteDrink $ roomNumber $ userClass $ homeTelephoneNumber $ homePostalAddress $ secretary $ personalTitle $ preferredDeliveryMethod $ businessCategory $ janetMailbox $ otherMailbox $ mobileTelephoneNumber $ pagerTelephoneNumber $ organizationalStatus $ mailPreferenceOption $ personalSignature ) )", + "( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ host ) )", + "( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( commonName $ description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) )", + "( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST commonName MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) )", + "( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST commonName MAY ( description $ seeAlso $ telephonenumber $ localityName $ organizationName $ organizationalUnitName ) )", + "( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST domainComponent MAY ( associatedName $ organizationName $ description $ businessCategory $ seeAlso $ searchGuide $ userPassword $ localityName $ stateOrProvinceName $ streetAddress $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) )", + "( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' SUP domain STRUCTURAL MAY ( commonName $ surname $ description $ seeAlso $ telephoneNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) )", + "( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' SUP domain STRUCTURAL MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ SOARecord $ CNAMERecord ) )", + "( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' DESC 'RFC1274: an object related to an domain' SUP top AUXILIARY MUST associatedDomain )", + "( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST friendlyCountryName )", + "( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization' SUP ( organization $ organizationalUnit ) STRUCTURAL MAY buildingName )", + "( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' SUP dsa STRUCTURAL MAY dSAQuality )", + "( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData' SUP top AUXILIARY MUST dsaQuality MAY ( subtreeMinimumQuality $ subtreeMaximumQuality ) )", + "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )", + "( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( userPassword $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ description ) )", + "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )", + "( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an Internet Protocol service' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description )", + "( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction of an IP protocol' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber $ description ) MAY description )", + "( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an ONC/RPC binding' SUP top STRUCTURAL MUST ( cn $ oncRpcNumber $ description ) MAY description )", + "( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a host, an IP device' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( l $ description $ manager ) )", + "( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of an IP network' SUP top STRUCTURAL MUST ( cn $ ipNetworkNumber ) MAY ( ipNetmaskNumber $ l $ description $ manager ) )", + "( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction of a netgroup' SUP top STRUCTURAL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )", + "( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstraction of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description )", + "( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) MAY description )", + "( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device with a MAC address' SUP top AUXILIARY MAY macAddress )", + "( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A device with boot parameters' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) )", + "( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) )" + ], + "structuralObjectClass": [ + "subentry" + ], + "subschemaSubentry": [ + "cn=Subschema" + ] + }, + "schema_entry": "cn=Subschema", + "type": "SchemaInfo" +} \ No newline at end of file diff --git a/tests/tests.ldif b/tests/tests.ldif deleted file mode 100644 index 1902373e..00000000 --- a/tests/tests.ldif +++ /dev/null @@ -1,200 +0,0 @@ -dn: o=test -objectClass: organization -o: test - -dn: ou=people,o=test -objectClass: organizationalUnit -ou: people - -dn: ou=groups,o=test -objectClass: organizationalUnit -ou: groups - -dn: ou=moregroups,o=test -objectClass: organizationalUnit -ou: moregroups - -dn: ou=mirror_groups,o=test -objectClass: organizationalUnit -ou: mirror_groups - - -dn: uid=alice,ou=people,o=test -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: posixAccount -cn: alice -uid: alice -userPassword: password -uidNumber: 1000 -gidNumber: 1000 -givenName: Alice -sn: Adams -homeDirectory: /home/alice - -dn: uid=bob,ou=people,o=test -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: posixAccount -cn: bob -uid: bob -userPassword: password -uidNumber: 1001 -gidNumber: 50 -givenName: Robert -sn: Barker -homeDirectory: /home/bob - -dn: uid=dreßler,ou=people,o=test -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: posixAccount -cn: dreßler -uid: dreßler -userPassword: password -uidNumber: 1002 -gidNumber: 50 -givenName: Wolfgang -sn: Dreßler -homeDirectory: /home/dressler - -dn: uid=nobody,ou=people,o=test -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: posixAccount -cn: nobody -uid: nobody -userPassword: password -uidNumber: 1003 -gidNumber: 50 -sn: nobody -homeDirectory: /home/nobody - -dn: uid=nonposix,ou=people,o=test -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -cn: nonposix -uid: nonposix -userPassword: password -sn: nonposix - - -# posixGroup objects -dn: cn=active_px,ou=groups,o=test -objectClass: posixGroup -cn: active_px -gidNumber: 1000 -memberUid: nonposix - -dn: cn=staff_px,ou=groups,o=test -objectClass: posixGroup -cn: staff_px -gidNumber: 1001 -memberUid: alice -memberUid: nonposix - -dn: cn=superuser_px,ou=groups,o=test -objectClass: posixGroup -cn: superuser_px -gidNumber: 1002 -memberUid: alice -memberUid: nonposix - - -# groupOfNames groups -dn: cn=empty_gon,ou=groups,o=test -cn: empty_gon -objectClass: groupOfNames -member: - -dn: cn=active_gon,ou=groups,o=test -cn: active_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=staff_gon,ou=groups,o=test -cn: staff_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=superuser_gon,ou=groups,o=test -cn: superuser_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=other_gon,ou=moregroups,o=test -cn: other_gon -objectClass: groupOfNames -member: uid=bob,ou=people,o=test - - -# groupOfNames objects for LDAPGroupQuery testing -dn: ou=query_groups,o=test -objectClass: organizationalUnit -ou: query_groups - -dn: cn=alice_gon,ou=query_groups,o=test -cn: alice_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=mutual_gon,ou=query_groups,o=test -cn: mutual_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test -member: uid=bob,ou=people,o=test - -dn: cn=bob_gon,ou=query_groups,o=test -cn: bob_gon -objectClass: groupOfNames -member: uid=bob,ou=people,o=test - -dn: cn=dreßler_gon,ou=query_groups,o=test -cn: dreßler_gon -objectClass: groupOfNames -member: uid=dreßler,ou=people,o=test - - -# groupOfNames objects for selective group mirroring. -dn: cn=mirror1,ou=mirror_groups,o=test -cn: mirror1 -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=mirror2,ou=mirror_groups,o=test -cn: mirror2 -objectClass: groupOfNames -member: - -dn: cn=mirror3,ou=mirror_groups,o=test -cn: mirror3 -objectClass: groupOfNames -member: uid=alice,ou=people,o=test - -dn: cn=mirror4,ou=mirror_groups,o=test -cn: mirror4 -objectClass: groupOfNames -member: - - -# Nested groups with a circular reference -dn: cn=parent_gon,ou=groups,o=test -cn: parent_gon -objectClass: groupOfNames -member: cn=nested_gon,ou=groups,o=test - -dn: CN=nested_gon,ou=groups,o=test -cn: nested_gon -objectClass: groupOfNames -member: uid=alice,ou=people,o=test -member: cn=circular_gon,ou=groups,o=test - -dn: cn=circular_gon,ou=groups,o=test -cn: circular_gon -objectClass: groupOfNames -member: cn=parent_gon,ou=groups,o=test diff --git a/tests/tests.py b/tests/tests.py index 7220f20d..87c8fddc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -27,12 +27,10 @@ import logging import os import pickle -import warnings from copy import deepcopy from unittest import mock -import ldap -import slapdtest +import ldap3 from django.contrib.auth import authenticate, get_backends from django.contrib.auth.models import Group, Permission, User from django.core.cache import cache @@ -40,6 +38,10 @@ from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +from ldap3 import Connection, Server +from ldap3.core.exceptions import LDAPException +from ldap3.core.results import RESULT_CODES, RESULT_SUCCESS +from ldap3.utils.ciDict import CaseInsensitiveDict from django_auth_ldap.backend import LDAPBackend, ldap_error, populate_user from django_auth_ldap.config import ( @@ -79,7 +81,7 @@ def spy_ldap(name): Patch the python-ldap method. The patched method records all calls and passes execution to the original method. """ - ldap_method = getattr(ldap.ldapobject.SimpleLDAPObject, name) + ldap_method = getattr(Connection, name) ldap_mock = mock.MagicMock() @functools.wraps(ldap_method) @@ -90,9 +92,7 @@ def wrapped_ldap_method(self, *args, **kwargs): def decorator(test): @functools.wraps(test) def wrapped_test(self, *args, **kwargs): - with mock.patch.object( - ldap.ldapobject.SimpleLDAPObject, name, wrapped_ldap_method - ): + with mock.patch.object(Connection, name, wrapped_ldap_method): return test(self, ldap_mock, *args, **kwargs) return wrapped_test @@ -112,6 +112,12 @@ def catch_signal(signal): class LDAPTest(TestCase): + ldap_uri = "ldap://test_server" + server = None + mock_server_new = None + mock_ldap3_sync = None + mock_ldap3_async = None + @classmethod def configure_logger(cls): logger = logging.getLogger("django_auth_ldap") @@ -130,40 +136,62 @@ def setUpClass(cls): cls.configure_logger() here = os.path.dirname(__file__) - cls.server = slapdtest.SlapdObject() - cls.server.suffix = "o=test" - cls.server.openldap_schema_files = [ - "core.schema", - "cosine.schema", - "inetorgperson.schema", - "nis.schema", - ] - cls.server.start() - with open(os.path.join(here, "tests.ldif")) as fp: - ldif = fp.read() - cls.server.ldapadd(ldif) + + def wrapped_server_new(_cls, host, *_, use_ssl=False, **__): + cls.mock_server_new.stop() + + def unwrapped_server_new(__cls, *args, **kwargs): + _server = object.__new__(__cls) + __cls.__init__(_server, *args, **kwargs) + return _server + + unmock_server_new = mock.patch.object( + Server, "__new__", unwrapped_server_new + ) + unmock_server_new.start() + + server = Server.from_definition( + host, + os.path.join(here, "test_server_info.json"), + os.path.join(here, "test_server_schema.json"), + use_ssl=use_ssl, + ) + + connection = Connection(server, client_strategy=ldap3.MOCK_ASYNC) + connection.strategy.entries_from_json( + os.path.join(here, "test_server_entries.json") + ) + + unmock_server_new.stop() + cls.mock_server_new.start() + + return server + + cls.mock_server_new = mock.patch.object(Server, "__new__", wrapped_server_new) + cls.mock_server_new.start() + + cls.server = Server(cls.ldap_uri) + + cls.mock_ldap3_sync = mock.patch("ldap3.SYNC", ldap3.MOCK_SYNC) + cls.mock_ldap3_sync.start() + + cls.mock_ldap3_async = mock.patch("ldap3.ASYNC", ldap3.MOCK_ASYNC) + cls.mock_ldap3_async.start() @classmethod def tearDownClass(cls): - cls.server.stop() + cls.mock_ldap3_async.stop() + cls.mock_ldap3_sync.stop() + cls.mock_server_new.stop() super().tearDownClass() def setUp(self): super().setUp() cache.clear() - def test_options(self): - self._init_settings( - USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - CONNECTION_OPTIONS={ldap.OPT_REFERRALS: 0}, - ) - user = authenticate(username="alice", password="password") - - self.assertEqual(user.ldap_user.connection.get_option(ldap.OPT_REFERRALS), 0) - def test_callable_server_uri(self): request = RequestFactory().get("/") - cb_mock = mock.Mock(return_value=self.server.ldap_uri) + cb_mock = mock.Mock(return_value=self.ldap_uri) self._init_settings( SERVER_URI=lambda request: cb_mock(request), @@ -178,29 +206,6 @@ def test_callable_server_uri(self): self.assertEqual(User.objects.count(), user_count + 1) cb_mock.assert_called_with(request) - def test_deprecated_callable_server_uri(self): - self._init_settings( - SERVER_URI=lambda: self.server.ldap_uri, - USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - ) - user_count = User.objects.count() - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - user = authenticate(username="alice", password="password") - - self.assertIs(user.has_usable_password(), False) - self.assertEqual(user.username, "alice") - self.assertEqual(User.objects.count(), user_count + 1) - self.assertEqual(len(w), 1) - self.assertEqual(w[0].category, DeprecationWarning) - self.assertEqual( - str(w[0].message), - "Update AUTH_LDAP_SERVER_URI callable tests.tests. to " - "accept a positional `request` argument. Support for callables " - "accepting no arguments will be removed in a future version.", - ) - def test_simple_bind(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") user_count = User.objects.count() @@ -211,23 +216,6 @@ def test_simple_bind(self): self.assertEqual(user.username, "alice") self.assertEqual(User.objects.count(), user_count + 1) - def test_default_settings(self): - class MyBackend(LDAPBackend): - default_settings = { - "SERVER_URI": self.server.ldap_uri, - "USER_DN_TEMPLATE": "uid=%(user)s,ou=people,o=test", - } - - backend = MyBackend() - - user_count = User.objects.count() - - user = backend.authenticate(None, username="alice", password="password") - - self.assertIs(user.has_usable_password(), False) - self.assertEqual(user.username, "alice") - self.assertEqual(User.objects.count(), user_count + 1) - @_override_settings( AUTHENTICATION_BACKENDS=[ "django_auth_ldap.backend.LDAPBackend", @@ -236,9 +224,7 @@ class MyBackend(LDAPBackend): ) def test_login_with_multiple_auth_backends(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) user = authenticate(username="alice", password="password") self.assertIsNotNone(user) @@ -251,9 +237,7 @@ def test_login_with_multiple_auth_backends(self): ) def test_bad_login_with_multiple_auth_backends(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) user = authenticate(username="invalid", password="i_do_not_exist") self.assertIsNone(user) @@ -263,7 +247,7 @@ def test_username_none(self): user = authenticate(username=None, password="password") self.assertIsNone(user) - @spy_ldap("simple_bind_s") + @spy_ldap("rebind") def test_simple_bind_escaped(self, mock): """ Bind with a username that requires escaping. """ self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") @@ -367,9 +351,7 @@ def test_existing_user(self): def test_existing_user_insensitive(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) User.objects.create(username="alice") @@ -405,9 +387,7 @@ def django_to_ldap_username(self, username): def test_search_bind(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) user_count = User.objects.count() @@ -416,26 +396,27 @@ def test_search_bind(self): self.assertIsNotNone(user) self.assertEqual(User.objects.count(), user_count + 1) - @spy_ldap("search_s") + @spy_ldap("search") def test_search_bind_escaped(self, mock): """ Search for a username that requires escaping. """ self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) user = authenticate(username="alice*", password="password") self.assertIsNone(user) mock.assert_called_once_with( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice\\2a)", None + "ou=people,o=test", + "(uid=alice\\2a)", + ldap3.SUBTREE, + attributes=ldap3.ALL_ATTRIBUTES, ) def test_search_bind_no_user(self): self._init_settings( USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uidNumber=%(user)s)" + "ou=people,o=test", ldap3.SUBTREE, "(uidNumber=%(user)s)" ) ) @@ -445,7 +426,7 @@ def test_search_bind_no_user(self): def test_search_bind_multiple_users(self): self._init_settings( - USER_SEARCH=LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=*)") + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=*)") ) user = authenticate(username="alice", password="password") @@ -454,9 +435,7 @@ def test_search_bind_multiple_users(self): def test_search_bind_bad_password(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) user = authenticate(username="alice", password="bogus") @@ -467,9 +446,7 @@ def test_search_bind_with_credentials(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="password", - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), ) user = authenticate(username="alice", password="password") @@ -477,15 +454,23 @@ def test_search_bind_with_credentials(self): self.assertIsNotNone(user) self.assertIsNotNone(user.ldap_user) self.assertEqual(user.ldap_user.dn, "uid=alice,ou=people,o=test") + + attrs = deepcopy(user.ldap_user.attrs) + object_classes = attrs.pop("objectClass") + + self.assertListEqual( + sorted(object_classes), + [ + "inetOrgPerson", + "organizationalPerson", + "person", + "posixAccount", + "top", + ], + ) self.assertEqual( - dict(user.ldap_user.attrs), + attrs, { - "objectClass": [ - "person", - "organizationalPerson", - "inetOrgPerson", - "posixAccount", - ], "cn": ["alice"], "uid": ["alice"], "userPassword": ["password"], @@ -501,9 +486,7 @@ def test_search_bind_with_bad_credentials(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), ) user = authenticate(username="alice", password="password") @@ -526,7 +509,7 @@ def test_cidict(self): user = authenticate(username="alice", password="password") - self.assertIsInstance(user.ldap_user.attrs, ldap.cidict.cidict) + self.assertIsInstance(user.ldap_user.attrs, CaseInsensitiveDict) def test_populate_user(self): self._init_settings( @@ -589,7 +572,7 @@ def test_populate_user_with_buggy_setter_raises_exception(self): with self.assertRaisesMessage(Exception, "Oops..."): backend.populate_user("alice") - @spy_ldap("search_s") + @spy_ldap("search") def test_populate_with_attrlist(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", @@ -603,7 +586,10 @@ def test_populate_with_attrlist(self, mock): # lookup user attrs mock.assert_called_once_with( - "uid=alice,ou=people,o=test", ldap.SCOPE_BASE, "(objectClass=*)", ["*", "+"] + "uid=alice,ou=people,o=test", + "(objectClass=*)", + ldap3.BASE, + attributes=["*", "+"], ) def test_bind_as_user(self): @@ -634,9 +620,7 @@ def test_auth_signal_ldap_error(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), ) def handle_ldap_error(sender, **kwargs): @@ -644,7 +628,7 @@ def handle_ldap_error(sender, **kwargs): with catch_signal(ldap_error) as handler: handler.side_effect = handle_ldap_error - with self.assertRaises(ldap.LDAPError): + with self.assertRaises(LDAPException): authenticate(username="alice", password="password") handler.assert_called_once() _args, kwargs = handler.call_args @@ -654,9 +638,7 @@ def test_populate_signal_ldap_error(self): self._init_settings( BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), ) backend = get_backend() @@ -684,7 +666,7 @@ def test_require_group(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=groupOfNames)" ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), REQUIRE_GROUP="cn=active_gon,ou=groups,o=test", @@ -712,7 +694,7 @@ def test_simple_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -726,7 +708,7 @@ def test_group_query_utf8(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -740,7 +722,7 @@ def test_negated_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -754,7 +736,7 @@ def test_or_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -773,7 +755,7 @@ def test_and_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -792,7 +774,7 @@ def test_nested_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -815,7 +797,7 @@ def test_require_group_as_group_query(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=query_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=MemberDNGroupType(member_attr="member"), @@ -833,11 +815,11 @@ def test_group_union(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearchUnion( LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=groupOfNames)" ), LDAPSearch( "ou=moregroups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), ), @@ -857,11 +839,11 @@ def test_nested_group_union(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearchUnion( LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=groupOfNames)" ), LDAPSearch( "ou=moregroups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), ), @@ -879,7 +861,7 @@ def test_nested_group_union(self): def test_denied_group(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), DENY_GROUP="cn=active_gon,ou=groups,o=test", ) @@ -893,7 +875,7 @@ def test_denied_group(self): def test_group_dns(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") @@ -911,7 +893,7 @@ def test_group_dns(self): def test_group_names(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), ) alice = authenticate(username="alice", password="password") @@ -924,7 +906,7 @@ def test_group_names(self): def test_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": LDAPGroupQuery("cn=active_gon,ou=groups,o=test"), @@ -949,7 +931,7 @@ def test_dn_group_membership(self): def test_user_flags_misconfigured(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": LDAPGroupQuery("cn=active_gon,ou=groups,o=test"), @@ -964,7 +946,7 @@ def test_user_flags_misconfigured(self): def test_posix_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={ "is_active": "cn=active_px,ou=groups,o=test", @@ -986,7 +968,7 @@ def test_posix_membership(self): def test_nested_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=NestedMemberDNGroupType(member_attr="member"), USER_FLAGS_BY_GROUP={ "is_active": "cn=parent_gon,ou=groups,o=test", @@ -1004,7 +986,7 @@ def test_nested_dn_group_membership(self): def test_posix_missing_attributes(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={"is_active": "cn=active_px,ou=groups,o=test"}, ) @@ -1016,7 +998,7 @@ def test_posix_missing_attributes(self): def test_dn_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) @@ -1040,7 +1022,7 @@ def test_group_permissions_ldap_error(self): BIND_DN="uid=bob,ou=people,o=test", BIND_PASSWORD="bogus", USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) @@ -1055,7 +1037,7 @@ def test_group_permissions_ldap_error(self): def test_empty_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) @@ -1074,7 +1056,7 @@ def test_posix_group_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True, @@ -1098,7 +1080,7 @@ def test_posix_group_permissions_no_gid(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True, @@ -1122,7 +1104,7 @@ def test_posix_group_permissions_no_gid(self): def test_foreign_user_permissions(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) @@ -1133,11 +1115,11 @@ def test_foreign_user_permissions(self): self.assertEqual(backend.get_group_permissions(alice), set()) - @spy_ldap("search_s") + @spy_ldap("search") def test_group_cache(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, CACHE_TIMEOUT=3600, @@ -1166,7 +1148,7 @@ def test_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=posixGroup)" ), GROUP_TYPE=PosixGroupType(), MIRROR_GROUPS=True, @@ -1183,7 +1165,7 @@ def test_nested_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( - "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" + "ou=groups,o=test", ldap3.SUBTREE, "(objectClass=groupOfNames)" ), GROUP_TYPE=NestedMemberDNGroupType(member_attr="member"), MIRROR_GROUPS=True, @@ -1220,7 +1202,7 @@ def test_group_mirroring_whitelist_update(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), @@ -1245,7 +1227,7 @@ def test_group_mirroring_whitelist_noop(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), @@ -1270,7 +1252,7 @@ def test_group_mirroring_blacklist_update(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), @@ -1295,7 +1277,7 @@ def test_group_mirroring_blacklist_noop(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", GROUP_SEARCH=LDAPSearch( "ou=mirror_groups,o=test", - ldap.SCOPE_SUBTREE, + ldap3.SUBTREE, "(objectClass=groupOfNames)", ), GROUP_TYPE=GroupOfNamesType(), @@ -1318,7 +1300,7 @@ def test_group_mirroring_blacklist_noop(self): def test_authorize_external_users(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, AUTHORIZE_ALL_USERS=True, @@ -1334,10 +1316,8 @@ def test_authorize_external_users(self): def test_authorize_external_unknown(self): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, AUTHORIZE_ALL_USERS=True, @@ -1374,7 +1354,7 @@ def test_populate_without_auth(self): USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", ALWAYS_UPDATE_USER=False, USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=GroupOfNamesType(), USER_FLAGS_BY_GROUP={ "is_active": "cn=active_gon,ou=groups,o=test", @@ -1411,47 +1391,79 @@ def test_populate_bogus_user(self): self.assertIsNone(bogus) - @spy_ldap("start_tls_s") - def test_start_tls_missing(self, mock): + @mock.patch.object(Connection, "start_tls", return_value=True) + def test_start_tls_missing(self, mock_start_tls): self._init_settings( - USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", START_TLS=False + SERVER_URI="test_server", + USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + START_TLS=False, ) authenticate(username="alice", password="password") - mock.assert_not_called() - @spy_ldap("start_tls_s") - def test_start_tls(self, mock): self._init_settings( - USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", START_TLS=True + SERVER_URI="ldap://test_server", + USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + START_TLS=False, ) authenticate(username="alice", password="password") - mock.assert_called_once() + mock_start_tls.assert_not_called() + + @mock.patch.object(Connection, "start_tls", return_value=True) + def test_start_tls(self, mock_start_tls): + self._init_settings( + SERVER_URI="ldap://test_server", + USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + START_TLS=True, + ) + + authenticate(username="alice", password="password") + mock_start_tls.assert_called_once() + + mock_start_tls.reset_mock() + + self._init_settings( + SERVER_URI="ldaps://test_server", + USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + START_TLS=False, + ) + + authenticate(username="alice", password="password") + mock_start_tls.assert_called_once() + + mock_start_tls.reset_mock() + + self._init_settings( + SERVER_URI="test_server", + USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + START_TLS=True, + ) + + authenticate(username="alice", password="password") + mock_start_tls.assert_called_once() def test_null_search_results(self): """ Make sure we're not phased by referrals. """ self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) authenticate(username="alice", password="password") def test_union_search(self): self._init_settings( USER_SEARCH=LDAPSearchUnion( - LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), - LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), + LDAPSearch("ou=groups,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), + LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), ) ) alice = authenticate(username="alice", password="password") self.assertIsNotNone(alice) - @spy_ldap("simple_bind_s") + @spy_ldap("rebind") def test_deny_empty_password(self, mock): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") @@ -1460,7 +1472,7 @@ def test_deny_empty_password(self, mock): self.assertIsNone(alice) mock.assert_not_called() - @spy_ldap("simple_bind_s") + @spy_ldap("rebind") def test_permit_empty_password(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", PERMIT_EMPTY_PASSWORD=True @@ -1471,7 +1483,7 @@ def test_permit_empty_password(self, mock): self.assertIsNone(alice) mock.assert_called_once() - @spy_ldap("simple_bind_s") + @spy_ldap("rebind") def test_permit_null_password(self, mock): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", PERMIT_EMPTY_PASSWORD=True @@ -1485,7 +1497,7 @@ def test_permit_null_password(self, mock): def test_pickle(self): self._init_settings( USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", - GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE), + GROUP_SEARCH=LDAPSearch("ou=groups,o=test", ldap3.SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr="member"), FIND_GROUP_PERMS=True, ) @@ -1507,17 +1519,35 @@ def test_pickle(self): self.assertIs(backend.has_perm(alice, "auth.add_user"), True) self.assertIs(backend.has_module_perms(alice, "auth"), True) - @mock.patch("ldap.ldapobject.SimpleLDAPObject.search_s") - def test_search_attrlist(self, mock_search): - backend = get_backend() - connection = backend.ldap.initialize(self.server.ldap_uri, bytes_mode=False) + def test_search_attrlist(self): + connection = Connection(self.ldap_uri) search = LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice)", ["*", "+"] - ) - search.execute(connection) - mock_search.assert_called_once_with( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=alice)", ["*", "+"] - ) + "ou=people,o=test", ldap3.SUBTREE, "(uid=alice)", ["*", "+"] + ) + + with mock.patch.object(connection, "search") as mock_search: + with mock.patch.object(connection, "get_response") as mock_result: + mock_search.return_value = 123 + mock_result.return_value = ( + [{"dn": "xyz", "attributes": dict(), "raw_attributes": list()}], + { + "result": RESULT_SUCCESS, + "description": RESULT_CODES[RESULT_SUCCESS], + "message": None, + "dn": None, + "referrals": None, + }, + ) + + search.execute(connection) + + mock_search.assert_called_once_with( + "ou=people,o=test", + "(uid=alice)", + ldap3.SUBTREE, + attributes=["*", "+"], + ) + mock_result.assert_called_once_with(mock_search.return_value) def test_override_authenticate_access_ldap_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") @@ -1531,12 +1561,10 @@ def authenticate_ldap_user(self, ldap_user, password): user = backend.authenticate(None, username="alice", password="password") self.assertEqual(user.ldap_user.foo, "bar") - @spy_ldap("search_s") + @spy_ldap("search") def test_dn_not_cached(self, mock): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ) + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)") ) for _ in range(2): user = authenticate(username="alice", password="password") @@ -1546,12 +1574,10 @@ def test_dn_not_cached(self, mock): # DN is not cached. self.assertIsNone(cache.get("django_auth_ldap.user_dn.alice")) - @spy_ldap("search_s") + @spy_ldap("search") def test_dn_cached(self, mock): self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), + USER_SEARCH=LDAPSearch("ou=people,o=test", ldap3.SUBTREE, "(uid=%(user)s)"), CACHE_TIMEOUT=60, ) for _ in range(2): @@ -1564,35 +1590,12 @@ def test_dn_cached(self, mock): cache.get("django_auth_ldap.user_dn.alice"), "uid=alice,ou=people,o=test" ) - def test_deprecated_cache_groups(self): - self._init_settings( - USER_SEARCH=LDAPSearch( - "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" - ), - CACHE_GROUPS=True, - ) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - user = authenticate(username="alice", password="password") - self.assertIsNotNone(user) - self.assertEqual(len(w), 1) - self.assertEqual(w[0].category, DeprecationWarning) - self.assertEqual( - str(w[0].message), - "Found deprecated setting AUTH_LDAP_CACHE_GROUP. Use " - "AUTH_LDAP_CACHE_TIMEOUT instead.", - ) - # DN is cached. - self.assertEqual( - cache.get("django_auth_ldap.user_dn.alice"), "uid=alice,ou=people,o=test" - ) - # # Utilities # def _init_settings(self, **kwargs): - kwargs.setdefault("SERVER_URI", self.server.ldap_uri) + kwargs.setdefault("SERVER_URI", self.ldap_uri) settings = {} for key, value in kwargs.items(): settings["AUTH_LDAP_%s" % key] = value diff --git a/tox.ini b/tox.ini index d4672d30..fb51a97d 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = black flake8 isort + mypy docs django22 django30 @@ -10,11 +11,14 @@ envlist = djangomaster [testenv] -commands = {envpython} -Wa -b -m django test --settings tests.settings +commands = + {envpython} --version + {envpython} -m django --version + {envpython} -Wa -b -m django test --settings tests.settings deps = django22: Django~=2.2.0 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 + django30: Django~=3.0.0 + django31: Django~=3.1.0 djangomaster: https://github.com/django/django/archive/master.tar.gz [testenv:black] @@ -32,6 +36,11 @@ deps = isort>=5.0.1 commands = isort --check --diff . skip_install = true +[testenv:mypy] +deps = mypy>=0.800 +commands = mypy django_auth_ldap +skip_install = true + [testenv:docs] deps = readme_renderer