Skip to content
Snippets Groups Projects
Commit 607bf2dd authored by Julian Rother's avatar Julian Rother
Browse files

Entry naming cleanup (Object -> ObjectEntry)

parent f95d1de0
Branches
Tags
No related merge requests found
Pipeline #8589 passed
......@@ -29,13 +29,16 @@ Distinguished Names
:members:
:special-members: __str__
Objects
Entries
-------
An LDAP server provides access to a tree of directory entries (the DIT).
There are different kinds of entries: Object entries holding actual data objects (see :class:`ObjectEntry`), alias entries providing alternative naming for other entries (not supported) and subentries holding administrative/operational information (see :class:`SubschemaSubentry` and :class:`RootDSE`).
.. autoclass:: ldapserver.AttributeDict
:members:
.. autoclass:: ldapserver.Object
.. autoclass:: ldapserver.Entry
:members:
.. autoclass:: ldapserver.RootDSE
......@@ -47,7 +50,7 @@ Objects
.. autodata:: ldapserver.WILDCARD_VALUE
:no-value:
.. autoclass:: ldapserver.ObjectTemplate
.. autoclass:: ldapserver.EntryTemplate
:members:
Schema
......
......@@ -57,7 +57,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
......
......@@ -12,14 +12,14 @@ class RequestHandler(ldapserver.LDAPRequestHandler):
def do_search(self, basedn, scope, filterobj):
yield from super().do_search(basedn, scope, filterobj)
yield self.subschema.Object('dc=example,dc=com', **{
yield self.subschema.ObjectEntry('dc=example,dc=com', **{
'objectClass': ['top', 'dcObject', 'organization'],
'structuralObjectClass': ['organization'],
})
user_gids = {}
for user in pwd.getpwall():
user_gids[user.pw_gid] = user_gids.get(user.pw_gid, set()) | {user.pw_name}
yield self.subschema.Object(self.subschema.DN('ou=users,dc=example,dc=com', uid=user.pw_name), **{
yield self.subschema.ObjectEntry(self.subschema.DN('ou=users,dc=example,dc=com', uid=user.pw_name), **{
'objectClass': ['top', 'organizationalperson', 'person', 'posixaccount'],
'structuralObjectClass': ['organizationalperson'],
'uid': [user.pw_name],
......@@ -29,7 +29,7 @@ class RequestHandler(ldapserver.LDAPRequestHandler):
})
for group in grp.getgrall():
members = set(group.gr_mem) | user_gids.get(group.gr_gid, set())
yield self.subschema.Object(self.subschema.DN('ou=groups,dc=example,dc=com', cn=group.gr_name), **{
yield self.subschema.ObjectEntry(self.subschema.DN('ou=groups,dc=example,dc=com', cn=group.gr_name), **{
'objectClass': ['top', 'groupOfUniqueNames', 'posixGroup'],
'structuralObjectClass': ['groupOfUniqueNames'],
'cn': [group.gr_name],
......
......@@ -4,5 +4,5 @@ from . import schema
from . import rfc4518_stringprep
from .dn import *
from .objects import *
from .entries import *
from .server import *
......@@ -4,7 +4,7 @@ import enum
from . import ldap, exceptions
from .dn import DN, RDN, RDNAssertion
__all__ = ['AttributeDict', 'Object', 'RootDSE', 'WILDCARD_VALUE', 'ObjectTemplate', 'SubschemaSubentry']
__all__ = ['AttributeDict', 'Entry', 'ObjectEntry', 'RootDSE', 'WILDCARD_VALUE', 'EntryTemplate', 'SubschemaSubentry']
class TypeKeysView(collections.abc.Set):
def __init__(self, attributes):
......@@ -110,10 +110,18 @@ class FilterResult(enum.Enum):
FALSE = enum.auto()
UNDEFINED = enum.auto()
class Object(AttributeDict):
class Entry(AttributeDict):
'''Base class for all directory entries
All entries implement the methods :any:`Entry.search` and
:any:`Entry.compare`. :class:`LDAPRequestHandler` uses them to process the
corresponding LDAP requests.
Entries also provide dict-like access to attributes. See
:class:`AttributeDict`.'''
def __init__(self, schema, dn, **attributes):
super().__init__(schema, **attributes)
#: Objects distinguished name (:class:`DN`)
#: Entry's distinguished name (:class:`DN`)
self.dn = DN(schema, dn)
def __search_match_dn(self, basedn, scope):
......@@ -220,10 +228,10 @@ class Object(AttributeDict):
self.__search_match_filter(filter_obj) == FilterResult.TRUE
def search(self, base_obj, scope, filter_obj, attributes, types_only):
'''Return SEARCH result for the object if it matches the operation
'''Return SEARCH result for the entry if it matches the operation
parameters
:param base_obj: DN of the base object
:param base_obj: DN of the base entry
:type base_obj: str
:param scope: Scope of base_obj
:type scope: ldap.SearchScope
......@@ -233,7 +241,7 @@ class Object(AttributeDict):
:type attributes: list of str
:param types_only: Omit values in :class:`ldap.PartialAttribute`
:type types_only: bool
:returns: SEARCH result for the object if it matches the operation
:returns: SEARCH result for the entry if it matches the operation
parameters, None otherwise
:rtype: ldap.SearchResultEntry or None
......@@ -259,15 +267,15 @@ class Object(AttributeDict):
return ldap.SearchResultEntry(str(self.dn), partial_attributes)
def compare(self, dn, attribute, value):
'''Return the result of the COMPARE operation applied to the object
'''Return the result of the COMPARE operation applied to the entry
:param dn: DN of the object to be compared
:param dn: DN of the entry to be compared
:type dn: str
:param attribute: Attribute OID or short descriptive name
:type attribute: str
:param value: Assertion value
:type value: bytes
:raises exceptions.LDAPNoSuchObject: if dn does not refer to the object
:raises exceptions.LDAPNoSuchObject: if dn does not refer to the entry
:raises exceptions.LDAPError: if operation results to anything other than
TRUE/FALSE
:return: True/False if COMPARE operation evaluates to TRUE/FALSE
......@@ -287,7 +295,14 @@ class Object(AttributeDict):
raise exceptions.LDAPUndefinedAttributeType() from exc
return attribute_type.match_equal(self.get(attribute_type, subtypes=True), value)
class RootDSE(Object):
class ObjectEntry(Entry):
'''Regular object entry'''
class RootDSE(Entry):
'''Root DSA-specific (server-specific) Entry
Root of the Directory Information Tree (DIT). It's always identified by the
emtpy DN. Provides information about the server, like supported features.'''
def __init__(self, schema, **attributes):
super().__init__(schema, DN(schema), **attributes)
self.setdefault('objectClass', ['top'])
......@@ -300,7 +315,7 @@ class RootDSE(Object):
class WildcardValue:
pass
#: Special wildcard value for :class:`ObjectTemplate`
#: Special wildcard value for :class:`EntryTemplate`
WILDCARD_VALUE = WildcardValue()
class TemplateFilterResult(enum.Enum):
......@@ -309,7 +324,7 @@ class TemplateFilterResult(enum.Enum):
UNDEFINED = enum.auto()
MAYBE_TRUE = enum.auto()
class ObjectTemplate(AttributeDict):
class EntryTemplate(AttributeDict):
def __init__(self, schema, parent_dn, rdn_attribute, **attributes):
super().__init__(schema, **attributes)
self.parent_dn = DN(schema, parent_dn)
......@@ -332,7 +347,7 @@ class ObjectTemplate(AttributeDict):
return False, AttributeDict(self.schema)
def __search_match_dn(self, basedn, scope):
'''Return whether objects from this template might match the provided parameters'''
'''Return whether entries from this template might match the provided parameters'''
return self.__match_extract_dn_constraints(basedn, scope)[0]
def __extract_dn_constraints(self, basedn, scope):
......@@ -467,15 +482,15 @@ class ObjectTemplate(AttributeDict):
return AttributeDict(self.schema)
def match_search(self, base_obj, scope, filter_obj):
'''Return whether objects based on this template might match the search parameters
'''Return whether entries based on this template might match the search parameters
:param base_obj: DN of the base object
:param base_obj: DN of the base entry
:type base_obj: str
:param scope: Scope of base_obj
:type scope: ldap.SearchScope
:param filter_obj: Search filter
:type filter_obj: ldap.Filter
:returns: True if objects based on this template might match the SEARCH
:returns: True if entries based on this template might match the SEARCH
parameters or False if they do not match
:rtype: bool'''
return self.__search_match_dn(DN.from_str(self.schema, base_obj), scope) and \
......@@ -483,21 +498,21 @@ class ObjectTemplate(AttributeDict):
TemplateFilterResult.MAYBE_TRUE)
def extract_search_constraints(self, base_obj, scope, filter_obj):
'''Return approximate value constraints for objects that match SEARCH parameters
'''Return approximate value constraints for entries that match SEARCH parameters
:returns: :class:`AttributeDict` with values that any object must have
:returns: :class:`AttributeDict` with values that any entry must have
to match potentially match SEARCH parameters
:rtype: AttributeDict
Example:
>>> subschema = SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema')
>>> template = subschema.ObjectTemplate('ou=users,dc=example,dc=com', 'cn', objectclass=['person', 'top'], sn=[WILDCARD_VALUE])
>>> template = subschema.EntryTemplate('ou=users,dc=example,dc=com', 'cn', objectclass=['person', 'top'], sn=[WILDCARD_VALUE])
>>> template.extract_search_constraints('cn=test,ou=users,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterEqual('sn', b'foobar'))
subschema.AttributeDict(cn=['test'], sn=['foobar'])
Note that the results are an approximation (i.e. some constrains may be
missing) and the quality may improve over time. Also note that every object
missing) and the quality may improve over time. Also note that every entry
that matches the SEARCH parameters also matches the constrains, but not
vice versa.'''
constraints = self.__extract_filter_constraints(filter_obj)
......@@ -505,17 +520,17 @@ class ObjectTemplate(AttributeDict):
constraints[key] += values
return constraints
def create_object(self, rdn_value, **attributes):
'''Instanciate :class:`Object` based on template
def create_entry(self, rdn_value, **attributes):
'''Instanciate :class:`Entry` based on template
:param rdn_value: RDN value for DN construction
:type rdn_value: any
:rtype: Object
:rtype: Entry
Template attributes set to :any:`WILDCARD_VALUE` are stripped. Only
template attributes set to :any:`WILDCARD_VALUE` may be overwritten
with `attributes`.'''
obj = Object(self.schema, DN(self.schema, self.parent_dn, **{self.rdn_attribute: rdn_value}))
obj = Entry(self.schema, DN(self.schema, self.parent_dn, **{self.rdn_attribute: rdn_value}))
for key, values in attributes.items():
if WILDCARD_VALUE not in self[key]:
raise ValueError(f'Cannot set attribute "{key}" that is not set to [WILDCARD_VALUE] in the template')
......@@ -525,8 +540,8 @@ class ObjectTemplate(AttributeDict):
obj[attribute_type] = values
return obj
class SubschemaSubentry(Object):
'''Special :class:`Object` providing information on a Schema'''
class SubschemaSubentry(Entry):
'''Subentry providing information on a :class:`Schema`'''
def __init__(self, schema, dn, **attributes):
super().__init__(schema, dn, **attributes)
self['subschemaSubentry'] = [self.dn]
......@@ -540,12 +555,12 @@ class SubschemaSubentry(Object):
# pylint: disable=invalid-name
#: Shorthand for :class:`AttributeDict`
self.AttributeDict = lambda **attributes: AttributeDict(schema, **attributes)
#: Shorthand for :class:`Object`
self.Object = lambda *args, **attributes: Object(schema, *args, subschemaSubentry=[self.dn], **attributes)
#: Shorthand for :class:`ObjectEntry`
self.ObjectEntry = lambda *args, **attributes: ObjectEntry(schema, *args, subschemaSubentry=[self.dn], **attributes)
#: Shorthand for :class:`RootDSE`
self.RootDSE = lambda **attributes: RootDSE(schema, subschemaSubentry=[self.dn], **attributes)
#: Shorthand for :class:`ObjectTemplate`
self.ObjectTemplate = lambda *args, **kwargs: ObjectTemplate(schema, *args, subschemaSubentry=[self.dn], **kwargs)
#: Shorthand for :class:`EntryTemplate`
self.EntryTemplate = lambda *args, **kwargs: EntryTemplate(schema, *args, subschemaSubentry=[self.dn], **kwargs)
class Wrapper:
def __init__(self, cls, schema):
self.cls = cls
......
......@@ -8,7 +8,7 @@ import random
import string
import itertools
from . import asn1, exceptions, ldap, schema, objects
from . import asn1, exceptions, ldap, schema, entries
__all__ = ['BaseLDAPRequestHandler', 'LDAPRequestHandler']
......@@ -168,7 +168,7 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
class LDAPRequestHandler(BaseLDAPRequestHandler):
#: :class:`SubschemaSubentry` object that describes the schema. Default
#: value uses :any:`schema.RFC4519_SCHEMA`. Returned by :any:`do_search`.
subschema = objects.SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema', cn=['Subschema'])
subschema = entries.SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema', cn=['Subschema'])
#: :class:`RootDSE` object containing information about the server, such
#: as supported extentions and SASL authentication mechansims. Content is
......@@ -437,11 +437,11 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
yield ldap.SearchResultDone(ldap.LDAPResultCode.success), [build_control()]
return
is_last = True
entries = 0
result_count = 0
time_start = time.perf_counter()
for entry, is_last in itertools.islice(iterator, 0, paged_control.size):
self.logger.debug('SEARCH entry %r', entry)
entries += 1
result_count += 1
yield entry
cookie = b''
if not is_last:
......@@ -450,8 +450,8 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
self.__paged_searches[cookie] = iterator, op
yield ldap.SearchResultDone(ldap.LDAPResultCode.success), [build_control(cookie=cookie)]
time_end = time.perf_counter()
self.logger.info('SEARCH dn=%r dn_scope=%s filter=%s attributes=%r page_cookie=%r entries=%d duration_seconds=%.3f',
op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), cookie, entries, time_end - time_start)
self.logger.info('SEARCH dn=%r dn_scope=%s filter=%s attributes=%r page_cookie=%r result_count=%d duration_seconds=%.3f',
op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), cookie, result_count, time_end - time_start)
def handle_search(self, op, controls=None):
self.logger.debug('SEARCH request dn=%r dn_scope=%s filter=%r attributes=%r',
......@@ -463,18 +463,18 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
if paged_control:
yield from self.__handle_search_paged(op, paged_control, controls)
return
entries = 0
result_count = 0
time_start = time.perf_counter()
for obj in self.do_search(op.baseObject, op.scope, op.filter):
entry = obj.search(op.baseObject, op.scope, op.filter, op.attributes, op.typesOnly)
if entry:
self.logger.debug('SEARCH entry %r', entry)
entries += 1
result_count += 1
yield entry
yield ldap.SearchResultDone(ldap.LDAPResultCode.success)
time_end = time.perf_counter()
self.logger.info('SEARCH dn=%r dn_scope=%s filter=\'%s\' attributes=%r entries=%d duration_seconds=%.3f',
op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), entries, time_end - time_start)
self.logger.info('SEARCH dn=%r dn_scope=%s filter=\'%s\' attributes=%r result_count=%d duration_seconds=%.3f',
op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), result_count, time_end - time_start)
def do_search(self, baseobj, scope, filterobj):
'''Return result candidates for a SEARCH operation
......@@ -487,16 +487,16 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
:param filterobj: Filter object
:type filterobj: ldap.Filter
:raises exceptions.LDAPError: on error
:returns: All LDAP objects that might match the parameters of the SEARCH
:returns: All entries that might match the parameters of the SEARCH
operation.
:rtype: Iterable of :class:`Object`
:rtype: Iterable of :class:`Entry`
The default implementation yields :any:`rootdse` and :any:`subschema`.
Both are importent for feature detection, so make sure to also return
them (e.g. with ``yield from super().do_search(...)``).
For every returned object :any:`Object.search` is called to filter out
non-matching objects and to construct the response.
For every returned object :any:`Entry.search` is called to filter out
non-matching entries and to construct the response.
Note that if this method is as an iterator, its execution may be paused
for extended periods of time or aborted prematurly.'''
......@@ -525,10 +525,10 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
:param value: Attribute value
:type value: bytes
:raises exceptions.LDAPError: on error
:returns: `Object` or None
:returns: `Entry` or None
The default implementation calls `do_search` and returns the first object
for which :any:`Object.compare` does not raise
for which :any:`Entry.compare` does not raise
:any:`exceptions.LDAPNoSuchObject` (i.e. the object has the requested DN).'''
objs = self.do_search(dn, ldap.SearchScope.baseObject, ldap.FilterPresent(attribute='objectClass'))
for obj in objs:
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment