diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..917bf8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +dist/ +/build +/doc/html +/ldapfilter_tab.py +.eggs/ +*.egg-info +/lib/*.egg-info +/lib/*.egg +/lib/activedirectory/protocol/*.so +*.pyc +*.log +*.egg +*.db +*.pid +pip-log.txt +.DS_Store +*swp +.python-version +.tox +venv/ +test.conf.atl diff --git a/README b/README deleted file mode 100644 index 983c855..0000000 --- a/README +++ /dev/null @@ -1,6 +0,0 @@ -Python-AD -========= - -This is Python-AD, an Active Directory client library for Python on UNIX/Linux -systems. For up to date information, see the project home page at -http://code.google.com/p/python-ad/. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3333b05 --- /dev/null +++ b/README.rst @@ -0,0 +1,56 @@ +Python-Active-Directory +======================= + +This is Python-AD, an Active Directory client library for Python on UNIX/Linux systems. + +**Note** - version 1.0 added support for Python >= 3.6 and version 2.0 will drop support for Python 2 + +Install +------- + +.. code:: bash + + $ pip install -e git+git@github.com:theatlantic/python-active-directory.git@v1.0.0+atl.2.0#egg=python-active-directory + + +Development +----------- + +Get the code +~~~~~~~~~~~~ + +.. code:: bash + + $ git clone git@github.com:theatlantic/python-active-directory.git + $ cd python-active-directory + + +Create virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Python 2: ``virtualenv venv`` +* Python 3: ``python -mvenv venv`` + +.. code:: bash + + $ . venv/bin/activate + $ pip install -e . + + +Testing +~~~~~~~ + +Version 1.0 switched to using pytest instead of nose, and added tox configuration +for supporting testing across various supported Python versions. + +.. code:: bash + + $ pip install tox + $ tox + +Special environment variables: + +* ``PYAD_TEST_CONFIG`` - Override the default test configuration file (formerly ``FREEADI_TEST_CONFIG``) +* ``PYAD_READONLY_CONFIG`` - Enable readonly tests, must be in the form of ``username:password@domain.tld`` + + diff --git a/doc/preface.xml b/doc/preface.xml index fded63d..0e09db6 100644 --- a/doc/preface.xml +++ b/doc/preface.xml @@ -8,7 +8,7 @@ This is the reference manual for Python-AD. It is the definitive reference for the Python-AD API. Other information on Python-AD, such as download instructions, installation instructions, and a tuturial, can be found on the - Python-AD project + Python-AD project page. diff --git a/doc/reference.xml b/doc/reference.xml index 8e14413..ae397cf 100644 --- a/doc/reference.xml +++ b/doc/reference.xml @@ -24,7 +24,7 @@ - from ad import Creds + from activedirectory import Creds @@ -118,7 +118,7 @@ - from ad import activate + from activedirectory import activate activate(creds) @@ -147,7 +147,7 @@ - from ad import Client + from activedirectory import Client @@ -471,7 +471,7 @@ ad package: - from ad import Locator + from activedirectory import Locator @@ -538,8 +538,8 @@ the exception as per the code fragment below: - from ad import Error as ADError - from ad import LDAPError + from activedirectory import Error as ADError + from activedirectory import LDAPError The ADError exception is raised for all errors that diff --git a/env.py b/env.py index a1dd4f3..f2cd499 100644 --- a/env.py +++ b/env.py @@ -13,7 +13,7 @@ # # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. - +from __future__ import print_function import os import os.path import sys @@ -49,8 +49,8 @@ def setenv(name, value): topdir, fname = os.path.split(abspath) bindir = os.path.join(topdir, 'bin') -print prepend_path('PATH', bindir) +print(prepend_path('PATH', bindir)) pythondir = os.path.join(topdir, 'lib') -print prepend_path('PYTHONPATH', pythondir) +print(prepend_path('PYTHONPATH', pythondir)) testconf = os.path.join(topdir, 'test.conf') -print setenv('FREEADI_TEST_CONFIG', testconf) +print(setenv('FREEADI_TEST_CONFIG', testconf)) diff --git a/gentab.py b/gentab.py index e8af0b9..7b9ae3c 100644 --- a/gentab.py +++ b/gentab.py @@ -11,9 +11,9 @@ # This script generates the PLY parser tables. Note: It needs to be run from # the top-level python-ad directory! -from ad.protocol.ldapfilter import Parser as LDAPFilterParser +from activedirectory.protocol.ldapfilter import Parser as LDAPFilterParser -os.chdir('lib/ad/protocol') +os.chdir('lib/activedirectory/protocol') parser = LDAPFilterParser() parser._write_parsetab() diff --git a/lib/activedirectory/__init__.py b/lib/activedirectory/__init__.py new file mode 100644 index 0000000..9e718ff --- /dev/null +++ b/lib/activedirectory/__init__.py @@ -0,0 +1,17 @@ +# +# This file is part of Python-AD. Python-AD is free software that is made +# available under the MIT license. Consult the file "LICENSE" that is +# distributed together with this file for the exact licensing terms. +# +# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file +# "AUTHORS" for a complete overview. + +from .core.exception import Error, LDAPError +from .core.constant import (LDAP_PORT, GC_PORT, AD_USERCTRL_ACCOUNT_DISABLED, + AD_USERCTRL_NORMAL_ACCOUNT, + AD_USERCTRL_WORKSTATION_ACCOUNT, + AD_USERCTRL_DONT_EXPIRE_PASSWORD) +from .core.client import Client +from .core.creds import Creds +from .core.locate import Locator +from .core.object import activate diff --git a/lib/ad/core/__init__.py b/lib/activedirectory/core/__init__.py similarity index 100% rename from lib/ad/core/__init__.py rename to lib/activedirectory/core/__init__.py diff --git a/lib/ad/core/client.py b/lib/activedirectory/core/client.py similarity index 86% rename from lib/ad/core/client.py rename to lib/activedirectory/core/client.py index 15c867a..8152237 100644 --- a/lib/ad/core/client.py +++ b/lib/activedirectory/core/client.py @@ -6,6 +6,7 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import re import dns import dns.resolver @@ -15,13 +16,13 @@ import ldap.controls import socket -from ad.core.exception import Error as ADError -from ad.core.object import factory, instance -from ad.core.creds import Creds -from ad.core.locate import Locator -from ad.core.constant import LDAP_PORT, GC_PORT -from ad.protocol import krb5 -from ad.util import compat +from .exception import Error as ADError +from .object import factory, instance +from .creds import Creds +from .locate import Locator +from .constant import LDAP_PORT, GC_PORT +from ..protocol import krb5 +from ..util import compat class Client(object): @@ -36,7 +37,7 @@ class Client(object): _referrals = False _pagesize = 500 - def __init__(self, domain): + def __init__(self, domain, creds=None): """Constructor.""" self.m_locator = None self.m_connections = None @@ -45,6 +46,7 @@ def __init__(self, domain): self.m_forest = None self.m_schema = None self.m_configuration = None + self.m_creds = creds def _locator(self): """Return our resource locator.""" @@ -54,10 +56,13 @@ def _locator(self): def _credentials(self): """Return our current AD credentials.""" - creds = instance(Creds) - if creds is None or not creds.principal(): - m = 'No current credentials or credentials not activated.' - raise ADError, m + if self.m_creds: + creds = self.m_creds + else: + creds = instance(Creds) + if creds is None or not creds.principal(): + m = 'No current credentials or credentials not activated.' + raise ADError(m) return creds def _fixup_scheme(self, scheme): @@ -66,9 +71,9 @@ def _fixup_scheme(self, scheme): scheme = 'ldap' elif isinstance(scheme, str): if scheme not in ('ldap', 'gc'): - raise ValueError, 'Illegal scheme: %s' % scheme + raise ValueError('Illegal scheme: %s' % scheme) else: - raise TypeError, 'Illegal scheme type: %s' % type(scheme) + raise TypeError('Illegal scheme type: %s' % type(scheme)) return scheme def _create_ldap_uri(self, servers, scheme=None): @@ -128,13 +133,13 @@ def _init_forest(self): 'configurationNamingContext') result = conn.search_s('', ldap.SCOPE_BASE, attrlist=attrs) if not result: - raise ADError, 'Could not search rootDSE of domain.' + raise ADError('Could not search rootDSE of domain.') finally: conn.unbind_s() dn, attrs = result[0] - self.m_forest = attrs['rootDomainNamingContext'][0] - self.m_schema = attrs['schemaNamingContext'][0] - self.m_configuration = attrs['configurationNamingContext'][0] + self.m_forest = attrs['rootDomainNamingContext'][0].decode('utf-8') + self.m_schema = attrs['schemaNamingContext'][0].decode('utf-8') + self.m_configuration = attrs['configurationNamingContext'][0].decode('utf-8') def domain(self): """Return the domain name of the current domain.""" @@ -182,13 +187,13 @@ def _init_naming_contexts(self): attrs = ('nCName',) result = conn.search_s(base, ldap.SCOPE_ONELEVEL, filter, attrs) if not result: - raise ADError, 'Could not search rootDSE of forest root.' + raise ADError('Could not search rootDSE of forest root.') finally: conn.unbind_s() naming_contexts = [] for res in result: dn, attrs = res - nc = attrs['nCName'][0].lower() + nc = attrs['nCName'][0].decode('utf-8').lower() naming_contexts.append(nc) self.m_naming_contexts = naming_contexts @@ -244,7 +249,7 @@ def _ldap_connection(self, base, server=None, scheme=None): uri = self._create_ldap_uri(servers, scheme) else: if not locator.check_domain_controller(server, domain, role): - raise ADError, 'Unsuitable server provided.' + raise ADError('Unsuitable server provided.') uri = self._create_ldap_uri([server], scheme) creds = self._credentials() creds._resolve_servers_for_domain(domain) @@ -262,7 +267,7 @@ def close(self): def _remove_empty_search_entries(self, result): """Remove empty search entries from a search result.""" # What I have seen so far these entries are always LDAP referrals - return filter(lambda x: x[0] is not None, result) + return [x for x in result if x[0] is not None] re_range = re.compile('([^;]+);[Rr]ange=([0-9]+)(?:-([0-9]+|\\*))?') @@ -280,7 +285,7 @@ def _retrieve_all_ranges(self, dn, key, attrs): hi = int(hi) except ValueError: m = 'Error while retrieving multi-valued attributes.' - raise ADError, m + raise ADError(m) rqattrs = ('%s;range=%s-*' % (type, hi+1),) filter = '(distinguishedName=%s)' % dn result = conn.search_s(base, ldap.SCOPE_SUBTREE, filter, rqattrs) @@ -298,7 +303,7 @@ def _retrieve_all_ranges(self, dn, key, attrs): break else: m = 'Error while retrieving multi-valued attributes.' - raise ADError, m + raise ADError(m) values += attrs2[key2] hi = hi2 attrs[type] = values @@ -317,7 +322,7 @@ def _fixup_filter(self, filter): if filter is None: filter = '(objectClass=*)' elif not isinstance(filter, str): - raise TypeError, 'Illegal filter type: %s' % type(filter) + raise TypeError('Illegal filter type: %s' % type(filter)) return filter def _fixup_base(self, base): @@ -325,7 +330,7 @@ def _fixup_base(self, base): if base is None: base = self.dn_from_domain_name(self.domain()) elif not isinstance(base, str): - raise TypeError, 'Illegal search base type: %s' % type(base) + raise TypeError('Illegal search base type: %s' % type(base)) return base def _fixup_scope(self, scope): @@ -341,9 +346,9 @@ def _fixup_scope(self, scope): elif isinstance(scope, int): if scope not in (ldap.SCOPE_BASE, ldap.SCOPE_ONELEVEL, ldap.SCOPE_SUBTREE): - raise ValueError, 'Illegal scope: %s' % scope + raise ValueError('Illegal scope: %s' % scope) else: - raise TypeError, 'Illegal scope type: %s' % type(scope) + raise TypeError('Illegal scope type: %s' % type(scope)) return scope def _fixup_attrs(self, attrs): @@ -353,15 +358,14 @@ def _fixup_attrs(self, attrs): elif isinstance(attrs, list) or isinstance(attrs, tuple): for item in attrs: if not isinstance(item, str): - raise TypeError, 'Expecting sequence of strings.' + raise TypeError('Expecting sequence of strings.') else: - raise TypeError, 'Expecting sequence of strings.' + raise TypeError('Expecting sequence of strings.') return attrs def _search_with_paged_results(self, conn, filter, base, scope, attrs): """Perform an ldap search operation with paged results.""" - ctrl = ldap.controls.SimplePagedResultsControl( - ldap.LDAP_CONTROL_PAGE_OID, True, (self._pagesize, '')) + ctrl = compat.SimplePagedResultsControl(self._pagesize) result = [] while True: msgid = conn.search_ext(base, scope, filter, attrs, @@ -369,11 +373,14 @@ def _search_with_paged_results(self, conn, filter, base, scope, attrs): type, data, msgid, ctrls = conn.result3(msgid) result += data rctrls = [ c for c in ctrls - if c.controlType == ldap.LDAP_CONTROL_PAGE_OID ] + if c.controlType == compat.LDAP_CONTROL_PAGED_RESULTS ] if not rctrls: m = 'Server does not honour paged results.' - raise ADError, m - est, cookie = rctrls[0].controlValue + raise ADError(m) + + size = rctrls[0].size + cookie = rctrls[0].cookie + if not cookie: break ctrl.controlValue = (self._pagesize, cookie) @@ -398,10 +405,10 @@ def search(self, filter=None, base=None, scope=None, attrs=None, if base == '': if server is None: m = 'A server must be specified when querying rootDSE' - raise ADError, m + raise ADError(m) if scope != ldap.SCOPE_BASE: m = 'Search scope must be base when querying rootDSE' - raise ADError, m + raise ADError(m) conn = self._ldap_connection(base, server, scheme) if base == '': # search rootDSE does not honour paged results @@ -416,19 +423,19 @@ def search(self, filter=None, base=None, scope=None, attrs=None, def _fixup_add_list(self, attrs): """Check the `attrs' arguments to add().""" if not isinstance(attrs, list) and not isinstance(attrs, tuple): - raise TypeError, 'Expecting list of 2-tuples %s.' + raise TypeError('Expecting list of 2-tuples %s.') for item in attrs: if not isinstance(item, tuple) and not isinstance(item, list) \ or not len(item) == 2: - raise TypeError, 'Expecting list of 2-tuples.' + raise TypeError('Expecting list of 2-tuples.') for type,values in attrs: if not isinstance(type, str): - raise TypeError, 'List items must be 2-tuple of (str, [str]).' + raise TypeError('List items must be 2-tuple of (str, [str]).') if not isinstance(values, list) and not isinstance(values, tuple): - raise TypeError, 'List items must be 2-tuple of (str, [str]).' + raise TypeError('List items must be 2-tuple of (str, [str]).') for val in values: if not isinstance(val, str): - raise TypeError, 'List items must be 2-tuple of (str, [str]).' + raise TypeError('List items must be 2-tuple of (str, [str]).') return attrs def add(self, dn, attrs, server=None): @@ -453,27 +460,27 @@ def _fixup_modify_operation(self, op): elif op == 'delete': op = ldap.MOD_DELETE elif op not in (ldap.MOD_ADD, ldap.MOD_REPLACE, ldap.MOD_DELETE): - raise ValueError, 'Illegal modify operation: %s' % op + raise ValueError('Illegal modify operation: %s' % op) return op def _fixup_modify_list(self, mods): """Check the `mods' argument to modify().""" if not isinstance(mods, list) and not isinstance(mods, tuple): - raise TypeError, 'Expecting list of 3-tuples.' + raise TypeError('Expecting list of 3-tuples.') for item in mods: if not isinstance(item, tuple) and not isinstance(item, list) \ or not len(item) == 3: - raise TypeError, 'Expecting list of 3-tuples.' + raise TypeError('Expecting list of 3-tuples.') result = [] for op,type,values in mods: op = self._fixup_modify_operation(op) if not isinstance(type, str): - raise TypeError, 'List items must be 3-tuple of (str, str, [str]).' + raise TypeError('List items must be 3-tuple of (str, str, [str]).') if not isinstance(values, list) and not isinstance(values, tuple): - raise TypeError, 'List items must be 3-tuple of (str, str, [str]).' + raise TypeError('List items must be 3-tuple of (str, str, [str]).') for val in values: if not isinstance(val, str): - raise TypeError, 'List item must be 3-tuple of (str, str, [str]).' + raise TypeError('List item must be 3-tuple of (str, str, [str]).') result.append((op,type,values)) return result @@ -525,8 +532,8 @@ def set_password(self, principal, password, server=None): creds._set_servers_for_domain(domain, [server]) try: krb5.set_password(principal, password) - except krb5.Error, err: - raise ADError, str(err) + except krb5.Error as err: + raise ADError(str(err)) if server is not None: creds._resolve_servers_for_domain(domain, force=True) @@ -543,7 +550,7 @@ def change_password(self, principal, oldpass, newpass, server=None): creds._set_servers_for_domain(domain, [server]) try: krb5.change_password(principal, oldpass, newpass) - except krb5.Error, err: - raise ADError, str(err) + except krb5.Error as err: + raise ADError(str(err)) if server is not None: creds._resolve_servers_for_domain(domain, force=True) diff --git a/lib/ad/core/constant.py b/lib/activedirectory/core/constant.py similarity index 100% rename from lib/ad/core/constant.py rename to lib/activedirectory/core/constant.py diff --git a/lib/ad/core/creds.py b/lib/activedirectory/core/creds.py similarity index 96% rename from lib/ad/core/creds.py rename to lib/activedirectory/core/creds.py index 5c77e47..696f3c9 100644 --- a/lib/ad/core/creds.py +++ b/lib/activedirectory/core/creds.py @@ -6,18 +6,19 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import os import time import logging import tempfile import ldap -from ad.core.object import factory -from ad.core.exception import Error -from ad.core.locate import Locator -from ad.core.locate import KERBEROS_PORT, KPASSWD_PORT -from ad.protocol import krb5 -from ad.util import compat +from .object import factory +from .exception import Error +from .locate import Locator +from .locate import KERBEROS_PORT, KPASSWD_PORT +from ..protocol import krb5 +from ..util import compat class Creds(object): @@ -64,7 +65,7 @@ def __init__(self, domain, use_system_config=False): self.m_config = None self.m_use_system_config = use_system_config self.m_config_cleanup = [] - self.m_logger = logging.getLogger('ad.core.creds') + self.m_logger = logging.getLogger('activedirectory.core.creds') def __del__(self): """Destructor. This releases all currently held credentials and cleans @@ -75,7 +76,7 @@ def load(self): """Load credentials from the OS.""" ccache = krb5.cc_default() if not os.access(ccache, os.R_OK): - raise Error, 'No ccache found' + raise Error('No ccache found') self.m_principal = krb5.cc_get_principal(ccache) self._init_ccache() krb5.cc_copy_creds(ccache, self.m_ccache) @@ -112,8 +113,8 @@ def acquire(self, principal, password=None, keytab=None, server=None): krb5.get_init_creds_password(principal, password) else: krb5.get_init_creds_keytab(principal, keytab) - except krb5.Error, err: - raise Error, str(err) + except krb5.Error as err: + raise Error(str(err)) self.m_principal = principal def release(self): @@ -191,7 +192,7 @@ def _resolve_servers_for_domain(self, domain, force=False): result = locator.locate_many(domain) if not result: m = 'No suitable domain controllers found for %s' % domain - raise Error, m + raise Error(m) self.m_domains[domain] = list(result) # Re-init every time self._init_config() @@ -237,7 +238,7 @@ def _write_config(self): """Write the Kerberos configuration file.""" assert self.m_config is not None ftmp = '%s.%d-tmp' % (self.m_config, os.getpid()) - fout = file(ftmp, 'w') + fout = open(ftmp, 'w') enctypes = ' '.join(self._supported_enctypes()) try: fout.write('# krb5.conf generated by Python-AD at %s\n' % diff --git a/lib/ad/core/exception.py b/lib/activedirectory/core/exception.py similarity index 91% rename from lib/ad/core/exception.py rename to lib/activedirectory/core/exception.py index 662b10c..06cd484 100644 --- a/lib/ad/core/exception.py +++ b/lib/activedirectory/core/exception.py @@ -6,6 +6,7 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import ldap diff --git a/lib/ad/core/locate.py b/lib/activedirectory/core/locate.py similarity index 88% rename from lib/ad/core/locate.py rename to lib/activedirectory/core/locate.py index 14ba67c..3a48187 100644 --- a/lib/ad/core/locate.py +++ b/lib/activedirectory/core/locate.py @@ -6,19 +6,22 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import time import random import logging +import six import ldap import dns.resolver import dns.reversename import dns.exception -from ad.protocol import netlogon -from ad.protocol.netlogon import Client as NetlogonClient -from ad.core.exception import Error as ADError -from ad.util import compat +from ..protocol import netlogon +from ..protocol.netlogon import Client as NetlogonClient +from .exception import Error as ADError +from ..util import compat +from six.moves import range LDAP_PORT = 389 @@ -54,26 +57,28 @@ class Locator(object): _maxservers = 3 _timeout = 300 # cache entries for 5 minutes - def __init__(self, site=None): + def __init__(self, site=None, resolver=None, resolve_hostnames=False): """Constructor.""" self.m_site = site self.m_site_detected = False - self.m_logger = logging.getLogger('ad.core.locate') + self.m_logger = logging.getLogger('activedirectory.core.locate') self.m_cache = {} self.m_timeout = self._timeout + self.m_resolver = resolver or dns.resolver.get_default_resolver() + self.m_resolve_hostnames = resolve_hostnames def locate(self, domain, role=None): """Locate one domain controller.""" servers = self.locate_many(domain, role, maxservers=1) if not servers: m = 'Could not locate domain controller' - raise ADError, m + raise ADError(m) return servers[0] def locate_many(self, domain, role=None, maxservers=None): """Locate a list of up to `maxservers' of domain controllers.""" result = self.locate_many_ex(domain, role, maxservers) - result = [ r.hostname for r in result ] + result = [ six.ensure_text(r.hostname) for r in result ] return result def locate_many_ex(self, domain, role=None, maxservers=None): @@ -84,7 +89,7 @@ def locate_many_ex(self, domain, role=None, maxservers=None): if maxservers is None: maxservers = self._maxservers if role not in ('dc', 'gc', 'pdc'): - raise ValueError, 'Role should be one of "dc", "gc" or "pdc".' + raise ValueError('Role should be one of "dc", "gc" or "pdc".') if role == 'pdc': maxservers = 1 domain = domain.upper() @@ -125,6 +130,17 @@ def locate_many_ex(self, domain, role=None, maxservers=None): servers = self._select_domain_controllers(replies, role, maxservers, addresses) self.m_logger.debug('found %d domain controllers' % len(servers)) + + if self.m_resolve_hostnames: + for srv in servers: + hostname = srv.hostname.decode('utf-8') + try: + address = self._dns_query(hostname, 'A')[0].address + except IndexError: + continue + else: + srv.hostname = address.encode('utf-8') + now = time.time() self.m_cache[key] = (now, maxservers, servers) return servers @@ -147,8 +163,8 @@ def _dns_query(self, query, type): """Perform a DNS query.""" self.m_logger.debug('DNS query %s type %s' % (query, type)) try: - answer = dns.resolver.query(query, type) - except dns.exception.DNSException, err: + answer = self.m_resolver.query(query, type) + except dns.exception.DNSException as err: answer = [] self.m_logger.error('DNS query error: %s' % (str(err) or err.__doc__)) else: @@ -170,7 +186,7 @@ def _detect_site(self, domain): netlogon.query(addr, domain) replies += netlogon.call() self.m_logger.debug('%d replies' % len(replies)) - if replies >= 3: + if len(replies) >= 3: break if not replies: self.m_logger.error('could not detect site') @@ -184,12 +200,12 @@ def _detect_site(self, domain): sites = [ (value, key) for key,value in sites.items() ] sites.sort() self.m_logger.debug('site detected as %s' % sites[-1][1]) - return sites[0][1] + return six.ensure_text(sites[0][1]) def _order_dns_srv(self, answer): """Order the results of a DNS SRV query.""" answer = list(answer) - answer.sort(lambda x,y: x.priority - y.priority) + answer.sort(key=lambda x: x.priority) result = [] for i in range(len(answer)): if i == 0: @@ -269,7 +285,7 @@ def _check_domain_controller(self, reply, role): role == 'dc' and not (reply.flags & netlogon.SERVER_LDAP): self.m_logger.error('Role does not match') return False - if reply.q_domain.lower() != reply.domain.lower(): + if reply.q_domain.lower() != six.ensure_text(reply.domain).lower(): self.m_logger.error('Domain does not match') return False self.m_logger.debug('Controller is OK') @@ -298,13 +314,12 @@ def _select_domain_controllers(self, replies, role, maxservers, addresses): assert hasattr(reply, 'checked') if not reply.checked: continue - if self.m_site.lower() == reply.server_site.lower(): + if self.m_site.lower() == six.ensure_text(reply.server_site).lower(): local.append(reply) else: remote.append(reply) - local.sort(lambda x,y: cmp(addresses.index((x.q_hostname, x.q_port)), - addresses.index((y.q_hostname, y.q_port)))) - remote.sort(lambda x,y: cmp(x.q_timing, y.q_timing)) + local.sort(key=lambda a: addresses.index((a.q_hostname, a.q_port))) + remote.sort(key=lambda a: a.q_timing) self.m_logger.debug('Local DCs: %s' % ', '.join(['%s:%s' % (x.q_hostname, x.q_port) for x in local])) self.m_logger.debug('Remote DCs: %s' % ', '.join(['%s:%s' % diff --git a/lib/ad/core/object.py b/lib/activedirectory/core/object.py similarity index 84% rename from lib/ad/core/object.py rename to lib/activedirectory/core/object.py index 38bc275..f406d24 100644 --- a/lib/ad/core/object.py +++ b/lib/activedirectory/core/object.py @@ -7,10 +7,11 @@ # "AUTHORS" for a complete overview. +from __future__ import absolute_import def _singleton(cls, *args, **kwargs): """Return the single instance of a class, creating it if it does not exist.""" if not hasattr(cls, 'instance') or cls.instance is None: - obj = apply(cls, args, kwargs) + obj = cls(*args, **kwargs) cls.instance = obj return cls.instance @@ -23,8 +24,8 @@ def instance(cls): def factory(cls): """Create an instance of a class, creating it using the system specific rules.""" - from ad.core.locate import Locator - from ad.core.creds import Creds + from activedirectory.core.locate import Locator + from activedirectory.core.creds import Creds if issubclass(cls, Locator): return _singleton(Locator) elif issubclass(cls, Creds): @@ -35,7 +36,7 @@ def factory(cls): def activate(obj): """Activate `obj' to be the active instance of its class.""" - from ad.core.creds import Creds + from activedirectory.core.creds import Creds if isinstance(obj, Creds): obj._activate_config() obj._activate_ccache() diff --git a/lib/ad/protocol/__init__.py b/lib/activedirectory/protocol/__init__.py similarity index 100% rename from lib/ad/protocol/__init__.py rename to lib/activedirectory/protocol/__init__.py diff --git a/lib/ad/protocol/asn1.py b/lib/activedirectory/protocol/asn1.py similarity index 78% rename from lib/ad/protocol/asn1.py rename to lib/activedirectory/protocol/asn1.py index 6ccef0f..e7d15e1 100644 --- a/lib/ad/protocol/asn1.py +++ b/lib/activedirectory/protocol/asn1.py @@ -6,6 +6,11 @@ # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the # file "AUTHORS" for a complete overview. +from __future__ import absolute_import +import re +import six +from six.moves import map +from six.moves import range Boolean = 0x01 Integer = 0x02 OctetString = 0x04 @@ -23,8 +28,6 @@ ClassContext = 0x80 ClassPrivate = 0xc0 -import re - class Error(Exception): """ASN1 error""" @@ -44,7 +47,7 @@ def start(self): def enter(self, nr, cls=None): """Start a constructed data value.""" if self.m_stack is None: - raise Error, 'Encoder not initialized. Call start() first.' + raise Error('Encoder not initialized. Call start() first.') if cls is None: cls = ClassUniversal self._emit_tag(nr, TypeConstructed, cls) @@ -53,10 +56,10 @@ def enter(self, nr, cls=None): def leave(self): """Finish a constructed data value.""" if self.m_stack is None: - raise Error, 'Encoder not initialized. Call start() first.' + raise Error('Encoder not initialized. Call start() first.') if len(self.m_stack) == 1: - raise Error, 'Tag stack is empty.' - value = ''.join(self.m_stack[-1]) + raise Error('Tag stack is empty.') + value = b''.join(self.m_stack[-1]) del self.m_stack[-1] self._emit_length(len(value)) self._emit(value) @@ -64,12 +67,14 @@ def leave(self): def write(self, value, nr=None, typ=None, cls=None): """Write a primitive data value.""" if self.m_stack is None: - raise Error, 'Encoder not initialized. Call start() first.' + raise Error('Encoder not initialized. Call start() first.') if nr is None: - if isinstance(value, int) or isinstance(value, long): + if isinstance(value, six.integer_types): nr = Integer - elif isinstance(value, str) or isinstance(value, unicode): + elif isinstance(value, six.string_types): nr = OctetString + if isinstance(value, six.text_type): + value = value.encode('utf-8') elif value is None: nr = Null if typ is None: @@ -84,10 +89,10 @@ def write(self, value, nr=None, typ=None, cls=None): def output(self): """Return the encoded output.""" if self.m_stack is None: - raise Error, 'Encoder not initialized. Call start() first.' + raise Error('Encoder not initialized. Call start() first.') if len(self.m_stack) != 1: - raise Error, 'Stack is not empty.' - output = ''.join(self.m_stack[0]) + raise Error('Stack is not empty.') + output = b''.join(self.m_stack[0]) return output def _emit_tag(self, nr, typ, cls): @@ -100,11 +105,11 @@ def _emit_tag(self, nr, typ, cls): def _emit_tag_short(self, nr, typ, cls): """Emit a short (< 31 bytes) tag.""" assert nr < 31 - self._emit(chr(nr | typ | cls)) + self._emit(six.int2byte(nr | typ | cls)) def _emit_tag_long(self, nr, typ, cls): """Emit a long (>= 31 bytes) tag.""" - head = chr(typ | cls | 0x1f) + head = six.int2byte(typ | cls | 0x1f) self._emit(head) values = [] values.append((nr & 0x7f)) @@ -113,7 +118,7 @@ def _emit_tag_long(self, nr, typ, cls): values.append((nr & 0x7f) | 0x80) nr >>= 7 values.reverse() - values = map(chr, values) + values = list(map(six.int2byte, values)) for val in values: self._emit(val) @@ -127,7 +132,7 @@ def _emit_length(self, length): def _emit_length_short(self, length): """Emit the short length form (< 128 octets).""" assert length < 128 - self._emit(chr(length)) + self._emit(six.int2byte(length)) def _emit_length_long(self, length): """Emit the long length form (>= 128 octets).""" @@ -136,17 +141,17 @@ def _emit_length_long(self, length): values.append(length & 0xff) length >>= 8 values.reverse() - values = map(chr, values) + values = list(map(six.int2byte, values)) # really for correctness as this should not happen anytime soon assert len(values) < 127 - head = chr(0x80 | len(values)) + head = six.int2byte(0x80 | len(values)) self._emit(head) for val in values: self._emit(val) def _emit(self, s): """Emit raw bytes.""" - assert isinstance(s, str) + assert isinstance(s, six.binary_type) self.m_stack[-1].append(s) def _encode_value(self, nr, value): @@ -165,7 +170,7 @@ def _encode_value(self, nr, value): def _encode_boolean(self, value): """Encode a boolean.""" - return value and '\xff' or '\x00' + return value and b'\xff' or b'\x00' def _encode_integer(self, value): """Encode an integer.""" @@ -192,8 +197,8 @@ def _encode_integer(self, value): assert i != len(values)-1 values[i] = 0x00 values.reverse() - values = map(chr, values) - return ''.join(values) + values = list(map(six.int2byte, values)) + return b''.join(values) def _encode_octet_string(self, value): """Encode an octetstring.""" @@ -202,17 +207,17 @@ def _encode_octet_string(self, value): def _encode_null(self): """Encode a Null value.""" - return '' + return b'' - _re_oid = re.compile('^[0-9]+(\.[0-9]+)+$') + _re_oid = re.compile(r'^[0-9]+(\.[0-9]+)+$') def _encode_object_identifier(self, oid): """Encode an object identifier.""" if not self._re_oid.match(oid): - raise Error, 'Illegal object identifier' - cmps = map(int, oid.split('.')) + raise Error('Illegal object identifier') + cmps = list(map(int, oid.split('.'))) if cmps[0] > 39 or cmps[1] > 39: - raise Error, 'Illegal object identifier' + raise Error('Illegal object identifier') cmps = [40 * cmps[0] + cmps[1]] + cmps[2:] cmps.reverse() result = [] @@ -222,8 +227,8 @@ def _encode_object_identifier(self, oid): cmp >>= 7 result.append(0x80 | (cmp & 0x7f)) result.reverse() - result = map(chr, result) - return ''.join(result) + result = list(map(six.int2byte, result)) + return b''.join(result) class Decoder(object): @@ -236,8 +241,8 @@ def __init__(self): def start(self, data): """Start processing `data'.""" - if not isinstance(data, str): - raise Error, 'Expecting string instance.' + if not isinstance(data, six.binary_type): + raise Error('Expecting %s instance.' % six.binary_type.__name__) self.m_stack = [[0, data]] self.m_tag = None @@ -245,7 +250,7 @@ def peek(self): """Return the value of the next tag without moving to the next TLV record.""" if self.m_stack is None: - raise Error, 'No input selected. Call start() first.' + raise Error('No input selected. Call start() first.') if self._end_of_input(): return None if self.m_tag is None: @@ -255,7 +260,7 @@ def peek(self): def read(self): """Read a simple value and move to the next TLV record.""" if self.m_stack is None: - raise Error, 'No input selected. Call start() first.' + raise Error('No input selected. Call start() first.') if self._end_of_input(): return None tag = self.peek() @@ -271,10 +276,10 @@ def eof(self): def enter(self): """Enter a constructed tag.""" if self.m_stack is None: - raise Error, 'No input selected. Call start() first.' + raise Error('No input selected. Call start() first.') nr, typ, cls = self.peek() if typ != TypeConstructed: - raise Error, 'Cannot enter a non-constructed tag.' + raise Error('Cannot enter a non-constructed tag.') length = self._read_length() bytes = self._read_bytes(length) self.m_stack.append([0, bytes]) @@ -283,19 +288,20 @@ def enter(self): def leave(self): """Leave the last entered constructed tag.""" if self.m_stack is None: - raise Error, 'No input selected. Call start() first.' + raise Error('No input selected. Call start() first.') if len(self.m_stack) == 1: - raise Error, 'Tag stack is empty.' + raise Error('Tag stack is empty.') del self.m_stack[-1] self.m_tag = None def _decode_boolean(self, bytes): """Decode a boolean value.""" if len(bytes) != 1: - raise Error, 'ASN1 syntax error' - if bytes[0] == '\x00': - return False - return True + raise Error('ASN1 syntax error') + byte = bytes[0] + if isinstance(byte, str): + byte = ord(byte) + return (byte != 0) def _read_tag(self): """Read a tag from the input.""" @@ -318,11 +324,13 @@ def _read_length(self): if byte & 0x80: count = byte & 0x7f if count == 0x7f: - raise Error, 'ASN1 syntax error' + raise Error('ASN1 syntax error') bytes = self._read_bytes(count) - bytes = [ ord(b) for b in bytes ] - length = 0L + bytes = [ b for b in bytes ] + length = 0 for byte in bytes: + if isinstance(byte, str): + byte = ord(byte) length = (length << 8) | byte try: length = int(length) @@ -353,10 +361,12 @@ def _read_byte(self): """Return the next input byte, or raise an error on end-of-input.""" index, input = self.m_stack[-1] try: - byte = ord(input[index]) + byte = input[index] except IndexError: - raise Error, 'Premature end of input.' + raise Error('Premature end of input.') self.m_stack[-1][0] += 1 + if isinstance(byte, str): + byte = ord(byte) return byte def _read_bytes(self, count): @@ -365,7 +375,7 @@ def _read_bytes(self, count): index, input = self.m_stack[-1] bytes = input[index:index+count] if len(bytes) != count: - raise Error, 'Premature end of input.' + raise Error('Premature end of input.') self.m_stack[-1][0] += count return bytes @@ -377,12 +387,16 @@ def _end_of_input(self): def _decode_integer(self, bytes): """Decode an integer value.""" - values = [ ord(b) for b in bytes ] + if six.PY2: + values = [ord(b) for b in bytes] + else: + values = [b for b in bytes] + # check if the integer is normalized if len(values) > 1 and \ (values[0] == 0xff and values[1] & 0x80 or values[0] == 0x00 and not (values[1] & 0x80)): - raise Error, 'ASN1 syntax error' + raise Error('ASN1 syntax error') negative = values[0] & 0x80 if negative: # make positive by taking two's complement @@ -394,7 +408,7 @@ def _decode_integer(self, bytes): break assert i > 0 values[i] = 0x00 - value = 0L + value = 0 for val in values: value = (value << 8) | val if negative: @@ -412,7 +426,7 @@ def _decode_octet_string(self, bytes): def _decode_null(self, bytes): """Decode a Null value.""" if len(bytes) != 0: - raise Error, 'ASN1 syntax error' + raise Error('ASN1 syntax error') return None def _decode_object_identifier(self, bytes): @@ -420,15 +434,17 @@ def _decode_object_identifier(self, bytes): result = [] value = 0 for i in range(len(bytes)): - byte = ord(bytes[i]) + byte = bytes[i] + if isinstance(byte, str): + byte = ord(byte) if value == 0 and byte == 0x80: - raise Error, 'ASN1 syntax error' + raise Error('ASN1 syntax error') value = (value << 7) | (byte & 0x7f) if not byte & 0x80: result.append(value) value = 0 if len(result) == 0 or result[0] > 1599: - raise Error, 'ASN1 syntax error' + raise Error('ASN1 syntax error') result = [result[0] // 40, result[0] % 40] + result[1:] - result = map(str, result) - return '.'.join(result) + result = [six.text_type(r).encode('utf-8') for r in result] + return b'.'.join(result) diff --git a/lib/ad/protocol/krb5.c b/lib/activedirectory/protocol/krb5.c similarity index 89% rename from lib/ad/protocol/krb5.c rename to lib/activedirectory/protocol/krb5.c index 76a259b..2435192 100644 --- a/lib/ad/protocol/krb5.c +++ b/lib/activedirectory/protocol/krb5.c @@ -277,7 +277,12 @@ k5_cc_default(PyObject *self, PyObject *args) return NULL; } + #if PY_MAJOR_VERSION >= 3 + ret = PyUnicode_FromString(name); + #else ret = PyString_FromString(name); + #endif + if (ret == NULL) return ret; @@ -348,7 +353,12 @@ k5_cc_get_principal(PyObject *self, PyObject *args) code = krb5_unparse_name(ctx, principal, &name); RETURN_ON_ERROR("krb5_unparse_name()", code); + #if PY_MAJOR_VERSION >= 3 + ret = PyUnicode_FromString(name); + #else ret = PyString_FromString(name); + #endif + if (ret == NULL) return ret; @@ -386,6 +396,17 @@ k5_c_valid_enctype(PyObject *self, PyObject *args) return ret; } +struct module_state { + PyObject *error; +}; + +#if PY_MAJOR_VERSION >= 3 +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) +#else +#define GETSTATE(m) (&_state) +static struct module_state _state; +#endif + static PyMethodDef k5_methods[] = { @@ -409,15 +430,67 @@ static PyMethodDef k5_methods[] = }; +#if PY_MAJOR_VERSION >= 3 + +static int k5_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int k5_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "krb5", + NULL, + sizeof(struct module_state), + k5_methods, + NULL, + k5_traverse, + k5_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit_krb5(void) + +#else +#define INITERROR return + void initkrb5(void) +#endif { PyObject *module, *dict; +#if !defined(__APPLE__) || !defined(__MACH__) initialize_krb5_error_table(); +#endif +#if PY_MAJOR_VERSION >= 3 + module = PyModule_Create(&moduledef); +#else module = Py_InitModule("krb5", k5_methods); +#endif + + if (module == NULL) + INITERROR; + dict = PyModule_GetDict(module); - k5_error = PyErr_NewException("freeadi.protocol.krb5.Error", NULL, NULL); - PyDict_SetItemString(dict, "Error", k5_error); + + struct module_state *st = GETSTATE(module); + k5_error = st->error = PyErr_NewException("freeadi.protocol.krb5.Error", NULL, NULL); + + PyDict_SetItemString(dict, "Error", st->error); + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif + } diff --git a/lib/ad/protocol/ldap.py b/lib/activedirectory/protocol/ldap.py similarity index 97% rename from lib/ad/protocol/ldap.py rename to lib/activedirectory/protocol/ldap.py index d27c658..2eec7b3 100644 --- a/lib/ad/protocol/ldap.py +++ b/lib/activedirectory/protocol/ldap.py @@ -6,8 +6,8 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. -from ad.protocol import asn1 -from ad.protocol import ldapfilter +from __future__ import absolute_import +from . import asn1, ldapfilter SCOPE_BASE = 0 @@ -189,9 +189,9 @@ def _check_tag(self, tag, id, typ=None, cls=None): typ = asn1.TypePrimitive if isinstance(id, tuple): if tag[0] not in id: - raise Error, 'LDAP syntax error' + raise Error('LDAP syntax error') elif id is not None: if tag[0] != id: - raise Error, 'LDAP syntax error' + raise Error('LDAP syntax error') if tag[1] != typ or tag[2] != cls: - raise Error, 'LDAP syntax error' + raise Error('LDAP syntax error') diff --git a/lib/ad/protocol/ldapfilter.py b/lib/activedirectory/protocol/ldapfilter.py similarity index 97% rename from lib/ad/protocol/ldapfilter.py rename to lib/activedirectory/protocol/ldapfilter.py index 5dbf9c2..5841540 100644 --- a/lib/ad/protocol/ldapfilter.py +++ b/lib/activedirectory/protocol/ldapfilter.py @@ -6,8 +6,9 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import re -from ad.util.parser import Parser as PLYParser +from activedirectory.util.parser import Parser as PLYParser class Error(Exception): diff --git a/lib/activedirectory/protocol/ldapfilter_tab.py b/lib/activedirectory/protocol/ldapfilter_tab.py new file mode 100644 index 0000000..31b3df6 --- /dev/null +++ b/lib/activedirectory/protocol/ldapfilter_tab.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import +from six.moves import zip + +# ldapfilter_tab.py +# This file is automatically generated. Do not edit. +_tabversion = '3.8' + +_lr_method = 'LALR' + +_lr_signature = '4F128CEA6E6C348C5E33FD02565B75DC' + +_lr_action_items = {'AND':([2,],[4,]),'APPROX':([5,],[14,]),'RPAREN':([3,7,9,10,11,12,13,18,19,20,21,22,23,24,25,26,27,28,],[11,20,22,23,-1,-5,-8,-14,-6,-4,-7,-3,-2,-9,-13,-12,-10,-11,]),'STRING':([2,14,15,16,17,],[5,25,26,27,28,]),'LTE':([5,],[17,]),'GTE':([5,],[15,]),'EQUALS':([5,],[16,]),'LPAREN':([0,4,6,8,11,13,20,22,23,],[2,2,2,2,-1,2,-4,-3,-2,]),'NOT':([2,],[8,]),'OR':([2,],[6,]),'PRESENT':([5,],[18,]),'$end':([1,11,20,22,23,],[0,-1,-4,-3,-2,]),} + +_lr_action = {} +for _k, _v in _lr_action_items.items(): + for _x,_y in zip(_v[0],_v[1]): + if not _x in _lr_action: _lr_action[_x] = {} + _lr_action[_x][_k] = _y +del _lr_action_items + +_lr_goto_items = {'and':([2,],[3,]),'filterlist':([4,6,13,],[12,19,24,]),'filter':([0,4,6,8,13,],[1,13,13,21,13,]),'item':([2,],[7,]),'not':([2,],[9,]),'or':([2,],[10,]),} + +_lr_goto = {} +for _k, _v in _lr_goto_items.items(): + for _x, _y in zip(_v[0], _v[1]): + if not _x in _lr_goto: _lr_goto[_x] = {} + _lr_goto[_x][_k] = _y +del _lr_goto_items +_lr_productions = [ + ("S' -> filter","S'",1,None,None,None), + ('filter -> LPAREN and RPAREN','filter',3,'p_filter','ldapfilter.py',108), + ('filter -> LPAREN or RPAREN','filter',3,'p_filter','ldapfilter.py',109), + ('filter -> LPAREN not RPAREN','filter',3,'p_filter','ldapfilter.py',110), + ('filter -> LPAREN item RPAREN','filter',3,'p_filter','ldapfilter.py',111), + ('and -> AND filterlist','and',2,'p_and','ldapfilter.py',116), + ('or -> OR filterlist','or',2,'p_or','ldapfilter.py',120), + ('not -> NOT filter','not',2,'p_not','ldapfilter.py',124), + ('filterlist -> filter','filterlist',1,'p_filterlist','ldapfilter.py',128), + ('filterlist -> filter filterlist','filterlist',2,'p_filterlist','ldapfilter.py',129), + ('item -> STRING EQUALS STRING','item',3,'p_item','ldapfilter.py',137), + ('item -> STRING LTE STRING','item',3,'p_item','ldapfilter.py',138), + ('item -> STRING GTE STRING','item',3,'p_item','ldapfilter.py',139), + ('item -> STRING APPROX STRING','item',3,'p_item','ldapfilter.py',140), + ('item -> STRING PRESENT','item',2,'p_item','ldapfilter.py',141), +] diff --git a/lib/ad/protocol/netlogon.py b/lib/activedirectory/protocol/netlogon.py similarity index 89% rename from lib/ad/protocol/netlogon.py rename to lib/activedirectory/protocol/netlogon.py index 6311d41..931956e 100644 --- a/lib/ad/protocol/netlogon.py +++ b/lib/activedirectory/protocol/netlogon.py @@ -6,14 +6,17 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import time import errno import socket import select import random -from ad.util import misc -from ad.protocol import asn1, ldap +from ..util import misc +from . import asn1, ldap +import six +from six.moves import range SERVER_PDC = 0x1 @@ -73,25 +76,25 @@ def _decode_rfc1035(self, _pointer=False): if _pointer == False: _pointer = [] while True: - tag = ord(self._read_byte()) + tag = self._read_byte() if tag == 0: break elif tag & 0xc0 == 0xc0: byte = self._read_byte() - ptr = ((tag & ~0xc0) << 8) + ord(byte) + ptr = ((tag & ~0xc0) << 8) + byte if ptr in _pointer: - raise Error, 'Cyclic pointer' + raise Error('Cyclic pointer') _pointer.append(ptr) saved, self.m_offset = self.m_offset, ptr result.append(self._decode_rfc1035(_pointer)) self.m_offset = saved break elif tag & 0xc0: - raise Error, 'Illegal tag' + raise Error('Illegal tag') else: s = self._read_bytes(tag) result.append(s) - result = '.'.join(result) + result = b'.'.join(result) return result def _try_convert_int(self, value): @@ -105,10 +108,10 @@ def _try_convert_int(self, value): def _decode_uint32(self): """Decode a 32-bit unsigned little endian integer from the current offset.""" - value = 0L + value = 0 for i in range(4): byte = self._read_byte() - value |= (long(ord(byte)) << i*8) + value |= (byte << i*8) value = self._try_convert_int(value) return value @@ -119,7 +122,7 @@ def _offset(self): def _set_offset(self, offset): """Set the current decoding offset.""" if offset < 0: - raise Error, 'Offset must be positive.' + raise Error('Offset must be positive.') self.m_offset = offset def _buffer(self): @@ -128,8 +131,8 @@ def _buffer(self): def _set_buffer(self, buffer): """Set the current buffer.""" - if not isinstance(buffer, str): - raise Error, 'Buffer must be plain string.' + if not isinstance(buffer, six.binary_type): + raise Error('Buffer must be bytes.') self.m_buffer = buffer def _read_byte(self, offset=None): @@ -140,8 +143,10 @@ def _read_byte(self, offset=None): else: update_offset = False if offset >= len(self.m_buffer): - raise Error, 'Premature end of input.' + raise Error('Premature end of input.') byte = self.m_buffer[offset] + if isinstance(byte, str): + byte = ord(byte) if update_offset: self.m_offset += 1 return byte @@ -156,7 +161,7 @@ def _read_bytes(self, count, offset=None): update_offset = False bytes = self.m_buffer[offset:offset+count] if len(bytes) != count: - raise Error, 'Premature end of input.' + raise Error('Premature end of input.') if update_offset: self.m_offset += count return bytes @@ -245,12 +250,12 @@ def _wait_for_replies(self, timeout): fds = [ self.m_socket.fileno() ] try: result = select.select(fds, [], [], timeleft) - except select.error, err: + except select.error as err: error = err.args[0] if error == errno.EINTR: continue # interrupted by signal else: - raise Error, str(err) # unrecoverable + raise Error(str(err)) # unrecoverable if not result[0]: continue # timeout assert fds == result[0] @@ -260,14 +265,14 @@ def _wait_for_replies(self, timeout): try: data, addr = self.m_socket.recvfrom(self._bufsize, socket.MSG_DONTWAIT) - except socket.error, err: + except socket.error as err: error = err.args[0] if error == errno.EINTR: continue # signal interrupt elif error == errno.EAGAIN: break # no data available now else: - raise Error, str(err) # unrecoverable + raise Error(str(err)) # unrecoverable try: hostname, port, domain, msgid = self.m_queries[addr] except KeyError: @@ -302,9 +307,8 @@ def _create_netlogon_query(self, domain, msgid): filter = '(&(DnsDomain=%s)(Host=%s)(NtVer=\\06\\00\\00\\00))' % \ (domain, hostname) attrs = ('NetLogon',) - query = client.create_search_request('', filter, attrs=attrs, + return client.create_search_request('', filter, attrs=attrs, scope=ldap.SCOPE_BASE, msgid=msgid) - return query def _parse_message_header(self, reply): """Parse an LDAP header and return the messageid and opcode.""" @@ -319,9 +323,9 @@ def _parse_netlogon_reply(self, reply): if not messages: return msgid, dn, attrs = messages[0] - if not attrs.get('netlogon'): - raise Error, 'No netlogon attribute received.' - data = attrs['netlogon'][0] + if not attrs.get(b'netlogon'): + raise Error('No netlogon attribute received.') + data = attrs[b'netlogon'][0] decoder = Decoder() decoder.start(data) result = decoder.parse() diff --git a/lib/ad/test/__init__.py b/lib/activedirectory/util/__init__.py similarity index 100% rename from lib/ad/test/__init__.py rename to lib/activedirectory/util/__init__.py diff --git a/lib/activedirectory/util/compat.py b/lib/activedirectory/util/compat.py new file mode 100644 index 0000000..397672c --- /dev/null +++ b/lib/activedirectory/util/compat.py @@ -0,0 +1,64 @@ +# +# This file is part of Python-AD. Python-AD is free software that is made +# available under the MIT license. Consult the file "LICENSE" that is +# distributed together with this file for the exact licensing terms. +# +# Python-AD is copyright (c) 2007-2009 by the Python-AD authors. See the +# file "AUTHORS" for a complete overview. + +from __future__ import absolute_import +import ldap +import ldap.dn + +from distutils import version + +# ldap.str2dn has been removed in python-ldap >= 2.3.6. We now need to use +# the version in ldap.dn. +try: + str2dn = ldap.dn.str2dn +except AttributeError: + str2dn = ldap.str2dn + +def disable_reverse_dns(): + # Possibly add in a Kerberos minimum version check as well... + return hasattr(ldap, 'OPT_X_SASL_NOCANON') + +if version.StrictVersion('2.4.0') <= version.StrictVersion(ldap.__version__): + LDAP_CONTROL_PAGED_RESULTS = ldap.CONTROL_PAGEDRESULTS +else: + LDAP_CONTROL_PAGED_RESULTS = ldap.LDAP_CONTROL_PAGE_OID + + +class SimplePagedResultsControl(ldap.controls.SimplePagedResultsControl): + """ + Python LDAP 2.4 and later breaks the API. This is an abstraction class + so that we can handle either. + http://planet.ergo-project.org/blog/jmeeuwen/2011/04/11/python-ldap-module-24-changes + """ + + def __init__(self, page_size=0, cookie=''): + if version.StrictVersion('2.4.0') <= version.StrictVersion(ldap.__version__): + ldap.controls.SimplePagedResultsControl.__init__( + self, + size=page_size, + cookie=cookie + ) + else: + ldap.controls.SimplePagedResultsControl.__init__( + self, + LDAP_CONTROL_PAGED_RESULTS, + critical, + (page_size, '') + ) + + def cookie(self): + if version.StrictVersion('2.4.0') <= version.StrictVersion(ldap.__version__): + return self.cookie + else: + return self.controlValue[1] + + def size(self): + if version.StrictVersion('2.4.0') <= version.StrictVersion(ldap.__version__): + return self.size + else: + return self.controlValue[0] diff --git a/lib/ad/util/log.py b/lib/activedirectory/util/log.py similarity index 95% rename from lib/ad/util/log.py rename to lib/activedirectory/util/log.py index afc0eec..7f1a205 100644 --- a/lib/ad/util/log.py +++ b/lib/activedirectory/util/log.py @@ -6,6 +6,7 @@ # Python-AD is copyright (c) 2007-2009 by the Python-AD authors. See the # file "AUTHORS" for a complete overview. +from __future__ import absolute_import import sys import logging diff --git a/lib/ad/util/misc.py b/lib/activedirectory/util/misc.py similarity index 94% rename from lib/ad/util/misc.py rename to lib/activedirectory/util/misc.py index ad7fbed..f48dd0a 100644 --- a/lib/ad/util/misc.py +++ b/lib/activedirectory/util/misc.py @@ -6,6 +6,7 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import socket diff --git a/lib/ad/util/parser.py b/lib/activedirectory/util/parser.py similarity index 94% rename from lib/ad/util/parser.py rename to lib/activedirectory/util/parser.py index 7b13b24..0ac9f3d 100644 --- a/lib/ad/util/parser.py +++ b/lib/activedirectory/util/parser.py @@ -6,6 +6,7 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import sys import os.path @@ -42,7 +43,8 @@ def parse(self, input, fname=None): self.m_input = input self.m_fname = fname parser = yacc.yacc(module=self, debug=0, - tabmodule=self._parsetab_name()) + tabmodule=self._parsetab_name(), + write_tables=0) parsed = parser.parse(lexer=lexer, tracking=True) return parsed diff --git a/lib/ad/__init__.py b/lib/ad/__init__.py deleted file mode 100644 index 2ad7d01..0000000 --- a/lib/ad/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -# This file is part of Python-AD. Python-AD is free software that is made -# available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file -# "AUTHORS" for a complete overview. - -from ad.core.exception import * -from ad.core.constant import * - -from ad.core.client import Client -from ad.core.creds import Creds -from ad.core.locate import Locator -from ad.core.object import activate diff --git a/lib/ad/core/test/test_client.py b/lib/ad/core/test/test_client.py deleted file mode 100644 index 647836b..0000000 --- a/lib/ad/core/test/test_client.py +++ /dev/null @@ -1,398 +0,0 @@ -# -# This file is part of Python-AD. Python-AD is free software that is made -# available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file -# "AUTHORS" for a complete overview. - -from nose.tools import assert_raises - -from ad.test.base import BaseTest -from ad.core.object import activate -from ad.core.client import Client -from ad.core.locate import Locator -from ad.core.constant import * -from ad.core.creds import Creds -from ad.core.exception import Error as ADError, LDAPError - - -class TestADClient(BaseTest): - """Test suite for ADClient""" - - def test_search(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - result = client.search('(objectClass=user)') - assert len(result) > 1 - - def _delete_user(self, client, name, server=None): - # Delete any user that may conflict with a newly to be created user - filter = '(|(cn=%s)(sAMAccountName=%s)(userPrincipalName=%s))' % \ - (name, name, '%s@%s' % (name, client.domain().upper())) - result = client.search('(&(objectClass=user)(sAMAccountName=%s))' % name, - server=server) - for res in result: - client.delete(res[0], server=server) - - def _create_user(self, client, name, server=None): - attrs = [] - attrs.append(('cn', [name])) - attrs.append(('sAMAccountName', [name])) - attrs.append(('userPrincipalName', ['%s@%s' % (name, client.domain().upper())])) - ctrl = AD_USERCTRL_ACCOUNT_DISABLED | AD_USERCTRL_NORMAL_ACCOUNT - attrs.append(('userAccountControl', [str(ctrl)])) - attrs.append(('objectClass', ['user'])) - dn = 'cn=%s,cn=users,%s' % (name, client.dn_from_domain_name(client.domain())) - self._delete_user(client, name, server=server) - client.add(dn, attrs, server=server) - return dn - - def _delete_obj(self, client, dn, server=None): - try: - client.delete(dn, server=server) - except (ADError, LDAPError): - pass - - def _create_ou(self, client, name, server=None): - attrs = [] - attrs.append(('objectClass', ['organizationalUnit'])) - attrs.append(('ou', [name])) - dn = 'ou=%s,%s' % (name, client.dn_from_domain_name(client.domain())) - self._delete_obj(client, dn, server=server) - client.add(dn, attrs, server=server) - return dn - - def test_add(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - user = self._create_user(client, 'test-usr') - self._delete_obj(client, user) - - def test_delete(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - dn = self._create_user(client, 'test-usr') - client.delete(dn) - - def test_modify(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - user = self._create_user(client, 'test-usr') - mods = [] - mods.append(('replace', 'sAMAccountName', ['test-usr-2'])) - client.modify(user, mods) - self._delete_obj(client, user) - - def test_modrdn(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') - if result: - client.delete(result[0][0]) - user = self._create_user(client, 'test-usr') - client.modrdn(user, 'cn=test-usr2') - result = client.search('(&(objectClass=user)(cn=test-usr2))') - assert len(result) == 1 - - def test_rename(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') - if result: - client.delete(result[0][0]) - user = self._create_user(client, 'test-usr') - client.rename(user, 'cn=test-usr2') - result = client.search('(&(objectClass=user)(cn=test-usr2))') - assert len(result) == 1 - user = result[0][0] - ou = self._create_ou(client, 'test-ou') - client.rename(user, 'cn=test-usr', ou) - newdn = 'cn=test-usr,%s' % ou - result = client.search('(&(objectClass=user)(cn=test-usr))') - assert len(result) == 1 - assert result[0][0].lower() == newdn.lower() - - def test_forest(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - forest = client.forest() - assert forest - assert forest.isupper() - - def test_domains(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - domains = client.domains() - for domain in domains: - assert domain - assert domain.isupper() - - def test_naming_contexts(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - naming_contexts = client.naming_contexts() - assert len(naming_contexts) >= 3 - - def test_search_all_domains(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - domains = client.domains() - for domain in domains: - base = client.dn_from_domain_name(domain) - result = client.search('(objectClass=*)', base=base, scope='base') - assert len(result) == 1 - - def test_search_schema(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - base = client.schema_base() - result = client.search('(objectClass=*)', base=base, scope='base') - assert len(result) == 1 - - def test_search_configuration(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - base = client.configuration_base() - result = client.search('(objectClass=*)', base=base, scope='base') - assert len(result) == 1 - - def _delete_group(self, client, dn, server=None): - try: - client.delete(dn, server=server) - except (ADError, LDAPError): - pass - - def _create_group(self, client, name, server=None): - attrs = [] - attrs.append(('cn', [name])) - attrs.append(('sAMAccountName', [name])) - attrs.append(('objectClass', ['group'])) - dn = 'cn=%s,cn=Users,%s' % (name, client.dn_from_domain_name(client.domain())) - self._delete_group(client, dn, server=server) - client.add(dn, attrs, server=server) - return dn - - def _add_user_to_group(self, client, user, group): - mods = [] - mods.append(('delete', 'member', [user])) - try: - client.modify(group, mods) - except (ADError, LDAPError): - pass - mods = [] - mods.append(('add', 'member', [user])) - client.modify(group, mods) - - def test_incremental_retrieval_of_multivalued_attributes(self): - self.require(ad_admin=True, expensive=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - user = self._create_user(client, 'test-usr') - groups = [] - for i in range(2000): - group = self._create_group(client, 'test-grp-%04d' % i) - self._add_user_to_group(client, user, group) - groups.append(group) - result = client.search('(sAMAccountName=test-usr)') - assert len(result) == 1 - dn, attrs = result[0] - assert attrs.has_key('memberOf') - assert len(attrs['memberOf']) == 2000 - self._delete_obj(client, user) - for group in groups: - self._delete_group(client, group) - - def test_paged_results(self): - self.require(ad_admin=True, expensive=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - users = [] - for i in range(2000): - user = self._create_user(client, 'test-usr-%04d' % i) - users.append(user) - result = client.search('(cn=test-usr-*)') - assert len(result) == 2000 - for user in users: - self._delete_obj(client, user) - - def test_search_rootdse(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - locator = Locator() - server = locator.locate(domain) - client = Client(domain) - result = client.search(base='', scope='base', server=server) - assert len(result) == 1 - dns, attrs = result[0] - assert attrs.has_key('supportedControl') - assert attrs.has_key('supportedSASLMechanisms') - - def test_search_server(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - locator = Locator() - server = locator.locate(domain) - client = Client(domain) - result = client.search('(objectClass=user)', server=server) - assert len(result) > 1 - - def test_search_gc(self): - self.require(ad_user=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) - activate(creds) - client = Client(domain) - result = client.search('(objectClass=user)', scheme='gc') - assert len(result) > 1 - for res in result: - dn, attrs = res - # accountExpires is always set, but is not a GC attribute - assert 'accountExpires' not in attrs - - def test_set_password(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - user = self._create_user(client, 'test-usr-1') - principal = 'test-usr-1@%s' % domain - client.set_password(principal, 'Pass123') - mods = [] - ctrl = AD_USERCTRL_NORMAL_ACCOUNT - mods.append(('replace', 'userAccountControl', [str(ctrl)])) - client.modify(user, mods) - creds = Creds(domain) - creds.acquire('test-usr-1', 'Pass123') - assert_raises(ADError, creds.acquire, 'test-usr-1', 'Pass321') - self._delete_obj(client, user) - - def test_set_password_target_pdc(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - locator = Locator() - pdc = locator.locate(domain, role='pdc') - user = self._create_user(client, 'test-usr-2', server=pdc) - principal = 'test-usr-2@%s' % domain - client.set_password(principal, 'Pass123', server=pdc) - mods = [] - ctrl = AD_USERCTRL_NORMAL_ACCOUNT - mods.append(('replace', 'userAccountControl', [str(ctrl)])) - client.modify(user, mods, server=pdc) - creds = Creds(domain) - creds.acquire('test-usr-2', 'Pass123', server=pdc) - assert_raises(ADError, creds.acquire, 'test-usr-2','Pass321', - server=pdc) - self._delete_obj(client, user, server=pdc) - - def test_change_password(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - user = self._create_user(client, 'test-usr-3') - principal = 'test-usr-3@%s' % domain - client.set_password(principal, 'Pass123') - mods = [] - ctrl = AD_USERCTRL_NORMAL_ACCOUNT - mods.append(('replace', 'userAccountControl', [str(ctrl)])) - mods.append(('replace', 'pwdLastSet', ['0'])) - client.modify(user, mods) - client.change_password(principal, 'Pass123', 'Pass456') - creds = Creds(domain) - creds.acquire('test-usr-3', 'Pass456') - assert_raises(ADError, creds.acquire, 'test-usr-3', 'Pass321') - self._delete_obj(client, user) - - def test_change_password_target_pdc(self): - self.require(ad_admin=True) - domain = self.domain() - creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) - activate(creds) - client = Client(domain) - locator = Locator() - pdc = locator.locate(domain, role='pdc') - user = self._create_user(client, 'test-usr-4', server=pdc) - principal = 'test-usr-4@%s' % domain - client.set_password(principal, 'Pass123', server=pdc) - mods = [] - ctrl = AD_USERCTRL_NORMAL_ACCOUNT - mods.append(('replace', 'userAccountControl', [str(ctrl)])) - mods.append(('replace', 'pwdLastSet', ['0'])) - client.modify(user, mods, server=pdc) - client.change_password(principal, 'Pass123', 'Pass456', server=pdc) - creds = Creds(domain) - creds.acquire('test-usr-4', 'Pass456', server=pdc) - assert_raises(ADError, creds.acquire, 'test-usr-4', 'Pass321', - server=pdc) - self._delete_obj(client, user, server=pdc) diff --git a/lib/ad/protocol/ldapfilter_tab.py b/lib/ad/protocol/ldapfilter_tab.py deleted file mode 100644 index de32df7..0000000 --- a/lib/ad/protocol/ldapfilter_tab.py +++ /dev/null @@ -1,42 +0,0 @@ - -# ldapfilter_tab.py -# This file is automatically generated. Do not edit. - -_lr_method = 'LALR' - -_lr_signature = 'o;C\x91;G\xcc[\x06G;\xa3\xa3R\xb9\x14' - -_lr_action_items = {'AND':([2,],[4,]),'APPROX':([5,],[14,]),'RPAREN':([3,7,9,10,11,12,13,18,19,20,21,22,23,24,25,26,27,28,],[11,20,22,23,-1,-5,-8,-14,-6,-4,-7,-3,-2,-9,-13,-12,-10,-11,]),'STRING':([2,14,15,16,17,],[5,25,26,27,28,]),'LTE':([5,],[17,]),'GTE':([5,],[15,]),'EQUALS':([5,],[16,]),'LPAREN':([0,4,6,8,11,13,20,22,23,],[2,2,2,2,-1,2,-4,-3,-2,]),'NOT':([2,],[8,]),'OR':([2,],[6,]),'PRESENT':([5,],[18,]),'$end':([1,11,20,22,23,],[0,-1,-4,-3,-2,]),} - -_lr_action = { } -for _k, _v in _lr_action_items.items(): - for _x,_y in zip(_v[0],_v[1]): - if not _lr_action.has_key(_x): _lr_action[_x] = { } - _lr_action[_x][_k] = _y -del _lr_action_items - -_lr_goto_items = {'and':([2,],[3,]),'filterlist':([4,6,13,],[12,19,24,]),'filter':([0,4,6,8,13,],[1,13,13,21,13,]),'item':([2,],[7,]),'not':([2,],[9,]),'or':([2,],[10,]),} - -_lr_goto = { } -for _k, _v in _lr_goto_items.items(): - for _x,_y in zip(_v[0],_v[1]): - if not _lr_goto.has_key(_x): _lr_goto[_x] = { } - _lr_goto[_x][_k] = _y -del _lr_goto_items -_lr_productions = [ - ("S'",1,None,None,None), - ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',108), - ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',109), - ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',110), - ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',111), - ('and',2,'p_and','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',116), - ('or',2,'p_or','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',120), - ('not',2,'p_not','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',124), - ('filterlist',1,'p_filterlist','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',128), - ('filterlist',2,'p_filterlist','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',129), - ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',137), - ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',138), - ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',139), - ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',140), - ('item',2,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',141), -] diff --git a/lib/ad/protocol/test/test_krb5.py b/lib/ad/protocol/test/test_krb5.py deleted file mode 100644 index c577653..0000000 --- a/lib/ad/protocol/test/test_krb5.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# This file is part of Python-AD. Python-AD is free software that is made -# available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file -# "AUTHORS" for a complete overview. - -import os -import stat -import pexpect - -from nose.tools import assert_raises -from ad.protocol import krb5 -from ad.test.base import BaseTest, Error - - -class TestKrb5(BaseTest): - """Test suite for protocol.krb5.""" - - def test_cc_default(self): - self.require(ad_user=True) - domain = self.domain().upper() - principal = '%s@%s' % (self.ad_user_account(), domain) - password = self.ad_user_password() - self.acquire_credentials(principal, password) - ccache = krb5.cc_default() - ccname, princ, creds = self.list_credentials(ccache) - assert princ.lower() == principal.lower() - assert len(creds) > 0 - assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) - - def test_cc_copy_creds(self): - self.require(ad_user=True) - domain = self.domain().upper() - principal = '%s@%s' % (self.ad_user_account(), domain) - password = self.ad_user_password() - self.acquire_credentials(principal, password) - ccache = krb5.cc_default() - cctmp = self.tempfile() - assert_raises(Error, self.list_credentials, cctmp) - krb5.cc_copy_creds(ccache, cctmp) - ccname, princ, creds = self.list_credentials(cctmp) - assert princ.lower() == principal.lower() - assert len(creds) > 0 - assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) - - def test_cc_get_principal(self): - self.require(ad_user=True) - domain = self.domain().upper() - principal = '%s@%s' % (self.ad_user_account(), domain) - password = self.ad_user_password() - self.acquire_credentials(principal, password) - ccache = krb5.cc_default() - princ = krb5.cc_get_principal(ccache) - assert princ.lower() == principal.lower() diff --git a/lib/ad/protocol/test/test_ldap.py b/lib/ad/protocol/test/test_ldap.py deleted file mode 100644 index 7178580..0000000 --- a/lib/ad/protocol/test/test_ldap.py +++ /dev/null @@ -1,48 +0,0 @@ -# -# This file is part of Python-AD. Python-AD is free software that is made -# available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file -# "AUTHORS" for a complete overview. - -import os.path -from ad.test.base import BaseTest -from ad.protocol import ldap - - -class TestLDAP(BaseTest): - """Test suite for ad.util.ldap.""" - - def test_encode_real_search_request(self): - client = ldap.Client() - filter = '(&(DnsDomain=FREEADI.ORG)(Host=magellan)(NtVer=\\06\\00\\00\\00))' - req = client.create_search_request('', filter, ('NetLogon',), - scope=ldap.SCOPE_BASE, msgid=4) - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'searchrequest.bin') - fin = file(fname) - buf = fin.read() - fin.close() - assert req == buf - - def test_decode_real_search_reply(self): - client = ldap.Client() - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'searchresult.bin') - fin = file(fname) - buf = fin.read() - fin.close() - reply = client.parse_message_header(buf) - assert reply == (4, 4) - reply = client.parse_search_result(buf) - assert len(reply) == 1 - msgid, dn, attrs = reply[0] - assert msgid == 4 - assert dn == '' - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'netlogon.bin') - fin = file(fname) - netlogon = fin.read() - fin.close() - assert attrs == { 'netlogon': [netlogon] } diff --git a/lib/ad/util/__init__.py b/lib/ad/util/__init__.py deleted file mode 100644 index 792d600..0000000 --- a/lib/ad/util/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/lib/ad/util/compat.py b/lib/ad/util/compat.py deleted file mode 100644 index c5d6d73..0000000 --- a/lib/ad/util/compat.py +++ /dev/null @@ -1,21 +0,0 @@ -# -# This file is part of Python-AD. Python-AD is free software that is made -# available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-AD is copyright (c) 2007-2009 by the Python-AD authors. See the -# file "AUTHORS" for a complete overview. - -import ldap -import ldap.dn - -# ldap.str2dn has been removed in python-ldap >= 2.3.6. We now need to use -# the version in ldap.dn. -try: - str2dn = ldap.dn.str2dn -except AttributeError: - str2dn = ldap.str2dn - -def disable_reverse_dns(): - # Possibly add in a Kerberos minimum version check as well... - return hasattr(ldap, 'OPT_X_SASL_NOCANON') diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..655cbb3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[nosetests] +# nocapture = true diff --git a/setup.py b/setup.py index 9a0a5bf..1505005 100644 --- a/setup.py +++ b/setup.py @@ -5,25 +5,49 @@ # # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. - from setuptools import setup, Extension + setup( - name = 'python-ad', - version = '0.9', - description = 'An AD client library for Python', - author = 'Geert Jansen', - author_email = 'geertj@gmail.com', - url = 'https://github.com/geertj/python-ad', - license = 'MIT', - classifiers = ['Development Status :: 4 - Beta', + name='python-active-directory', + version='1.0.5', + description='An Active Directory client library for Python', + long_description=open('README.rst').read(), + long_description_content_type='text/x-rst', + author='Geert Jansen', + author_email='programmers@theatlantic.com', + maintainer='The Atlantic', + maintainer_email='programmers@theatlantic.com', + url='https://github.com/theatlantic/python-active-directory', + license='MIT', + classifiers=[ + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python'], - package_dir = {'': 'lib'}, - packages = ['ad', 'ad.core', 'ad.protocol', 'ad.util'], - install_requires = [ 'python-ldap', 'dnspython', 'ply' ], - ext_modules = [Extension('ad.protocol.krb5', ['lib/ad/protocol/krb5.c'], - libraries=['krb5'])], - test_suite = 'nose.collector' + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], + package_dir={'': 'lib'}, + packages=[ + 'activedirectory', + 'activedirectory.core', + 'activedirectory.protocol', + 'activedirectory.util' + ], + tests_require=['nose', 'pexpect'], + install_requires=['python-ldap>=3.0', 'dnspython', 'ply>=3.8', 'six'], + ext_modules=[Extension( + 'activedirectory.protocol.krb5', + ['lib/activedirectory/protocol/krb5.c'], + libraries=['krb5'] + )], + zip_safe=False, # eggs are the devil. + test_suite='nose.collector' ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/ad/test/base.py b/tests/base.py similarity index 56% rename from lib/ad/test/base.py rename to tests/base.py index 7c0b91f..80994a8 100644 --- a/lib/ad/test/base.py +++ b/tests/base.py @@ -6,149 +6,165 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import os -import os.path import sys +import os.path +from io import open import tempfile + +import six +from six.moves import range +from six.moves.configparser import ConfigParser import pexpect +import pytest + +from activedirectory.util.log import enable_logging + -from nose import SkipTest -from ConfigParser import ConfigParser -from ad.util.log import enable_logging +def assert_raises(error_class, function, *args, **kwargs): + with pytest.raises(error_class): + function(*args, **kwargs) class Error(Exception): """Test error.""" +def dedent(self, s): + lines = s.splitlines() + for i in range(len(lines)): + lines[i] = lines[i].lstrip() + if lines and not lines[0]: + lines = lines[1:] + if lines and not lines[-1]: + lines = lines[:-1] + return '\n'.join(lines) + '\n' -class BaseTest(object): + +class Conf(object): """Base class for Python-AD tests.""" - @classmethod - def setup_class(cls): - config = ConfigParser() - fname = os.environ.get('FREEADI_TEST_CONFIG') + def __init__(self): + fname = os.environ.get( + 'PYAD_TEST_CONFIG', + os.path.join(os.path.dirname(__file__), 'test.conf.example') + ) if fname is None: - raise Error, 'Python-AD test configuration file not specified.' - if not os.access(fname, os.R_OK): - raise Error, 'Python-AD test configuration file does not exist.' - config.read(fname) - cls.c_config = config - cls.c_basedir = os.path.dirname(fname) - cls.c_iptables = None - cls.c_tempfiles = [] + raise Error('Python-AD test configuration file not specified.') + if not os.path.exists(fname): + raise Error('Python-AD test configuration file {} does not exist.'.format(fname)) + self.config = ConfigParser() + self.config.read(fname) + self.basedir = os.path.dirname(__file__) + self._iptables = None + self._domain = self.config.get('test', 'domain') + self._tempfiles = [] enable_logging() - @classmethod - def teardown_class(cls): - for fname in cls.c_tempfiles: + self.readonly_ad_creds = None + readonly_env = os.environ.get('PYAD_READONLY_CONFIG', None) + if readonly_env: + bits = readonly_env.rsplit('@', 1) + if len(bits) == 2: + creds, domain = bits + bits = creds.split(':', 1) + if len(bits) == 2: + self._domain = domain + self.readonly_ad_creds = bits + elif self.config.getboolean('test', 'readonly_ad_tests'): + self.readonly_ad_creds = [ + config.get('test', 'ad_user_account'), + config.get('test', 'ad_user_password'), + ] + + def teardown(self): + for fname in self._tempfiles: try: os.unlink(fname) except OSError: pass - cls.c_tempfiles = [] - - def config(self): - return self.c_config - - def _dedent(self, s): - lines = s.splitlines() - for i in range(len(lines)): - lines[i] = lines[i].lstrip() - if lines and not lines[0]: - lines = lines[1:] - if lines and not lines[-1]: - lines = lines[:-1] - return '\n'.join(lines) + '\n' + self._tempfiles = [] def tempfile(self, contents=None, remove=False): fd, name = tempfile.mkstemp() if contents: - os.write(fd, self._dedent(contents)) + os.write(fd, dedent(contents)) elif remove: os.remove(name) os.close(fd) - self.c_tempfiles.append(name) + self._tempfiles.append(name) return name - def basedir(self): - return self.c_basedir + def read_file(self, fname): + fname = os.path.join(self.basedir, fname) + with open(fname, 'rb') as fin: + buf = fin.read() + + return buf def require(self, ad_user=False, local_admin=False, ad_admin=False, firewall=False, expensive=False): if firewall: local_admin = True - config = self.config() - if ad_user and not config.getboolean('test', 'readonly_ad_tests'): - raise SkipTest, 'test disabled by configuration' - if not config.get('test', 'domain'): - raise SkipTest, 'ad tests enabled but no domain given' - if not config.get('test', 'ad_user_account') or \ - not config.get('test', 'ad_user_password'): - raise SkipTest, 'readonly ad tests enabled but no user/pw given' + config = self.config + if ad_user and not ( + self.readonly_ad_creds and all(self.readonly_ad_creds) + ): + raise pytest.skip('test disabled by configuration') if local_admin: if not config.getboolean('test', 'intrusive_local_tests'): - raise SkipTest, 'test disabled by configuration' + raise pytest.skip('test disabled by configuration') if not config.get('test', 'local_admin_account') or \ not config.get('test', 'local_admin_password'): - raise SkipTest, 'intrusive local tests enabled but no user/pw given' + raise pytest.skip('intrusive local tests enabled but no user/pw given') if ad_admin: if not config.getboolean('test', 'intrusive_ad_tests'): - raise SkipTest, 'test disabled by configuration' + raise pytest.skip('test disabled by configuration') if not config.get('test', 'ad_admin_account') or \ not config.get('test', 'ad_admin_password'): - raise SkipTest, 'intrusive ad tests enabled but no user/pw given' - if firewall and not self._iptables_supported(): - raise SkipTest, 'iptables/conntrack not available' + raise pytest.skip('intrusive ad tests enabled but no user/pw given') + if firewall and not self.iptables_supported: + raise pytest.skip('iptables/conntrack not available') if expensive and not config.getboolean('test', 'expensive_tests'): - raise SkipTest, 'test disabled by configuration' + raise pytest.skip('test disabled by configuration') def domain(self): - config = self.config() - domain = config.get('test', 'domain') - return domain + return self._domain def ad_user_account(self): self.require(ad_user=True) - account = self.config().get('test', 'ad_user_account') - return account + return self.readonly_ad_creds[0] def ad_user_password(self): self.require(ad_user=True) - password = self.config().get('test', 'ad_user_password') - return password + return self.readonly_ad_creds[1] def local_admin_account(self): self.require(local_admin=True) - account = self.config().get('test', 'local_admin_account') - return account + return self.config.get('test', 'local_admin_account') def local_admin_password(self): self.require(local_admin=True) - password = self.config().get('test', 'local_admin_password') - return password + return self.config.get('test', 'local_admin_password') def ad_admin_account(self): self.require(ad_admin=True) - account = self.config().get('test', 'ad_admin_account') - return account + return self.config.get('test', 'ad_admin_account') def ad_admin_password(self): self.require(ad_admin=True) - password = self.config().get('test', 'ad_admin_password') - return password + return self.config.get('test', 'ad_admin_password') def execute_as_root(self, command): self.require(local_admin=True) - child = pexpect.spawn('su -c "%s" %s' % \ - (command, self.local_admin_account())) + child = pexpect.spawn('su -c "%s" %s' % (command, self.local_admin_account())) child.expect('.*:') child.sendline(self.local_admin_password()) child.expect(pexpect.EOF) assert not child.isalive() if child.exitstatus != 0: m = 'Root command exited with status %s' % child.exitstatus - raise Error, m + raise Error(m) return child.before def acquire_credentials(self, principal, password, ccache=None): @@ -163,7 +179,7 @@ def acquire_credentials(self, principal, password, ccache=None): assert not child.isalive() if child.exitstatus != 0: m = 'Command kinit exited with status %s' % child.exitstatus - raise Error, m + raise Error(m) def list_credentials(self, ccache=None): if ccache is None: @@ -173,7 +189,7 @@ def list_credentials(self, ccache=None): child.expect('Ticket cache: ([a-zA-Z0-9_/.:-]+)\r\n') except pexpect.EOF: m = 'Command klist exited with status %s' % child.exitstatus - raise Error, m + raise Error(m) ccache = child.match.group(1) child.expect('Default principal: ([a-zA-Z0-9_/.:@-]+)\r\n') principal = child.match.group(1) @@ -190,16 +206,17 @@ def list_credentials(self, ccache=None): creds.append(child.match.group(1)) return ccache, principal, creds - def _iptables_supported(self): - if self.c_iptables is None: + @property + def iptables_supported(self): + if self._iptables is None: try: self.execute_as_root('iptables -L -n') self.execute_as_root('conntrack -L') except Error: - self.c_iptables = False + self._iptables = False else: - self.c_iptables = True - return self.c_iptables + self._iptables = True + return self._iptables def remove_network_blocks(self): self.require(local_admin=True, firewall=True) @@ -218,6 +235,7 @@ def block_outgoing_traffic(self, protocol, port): # the nat table is not enough. We also need to flush the conntrack # table that keeps state for NAT'ed connections even after the rule # that caused the NAT in the first place has been removed. - self.execute_as_root('iptables -t nat -A OUTPUT -m %s -p %s --dport %d' - ' -j DNAT --to-destination 127.0.0.1:9' % - (protocol, protocol, port)) + self.execute_as_root( + 'iptables -t nat -A OUTPUT -m %s -p %s --dport %d ' + '-j DNAT --to-destination 127.0.0.1:9' % (protocol, protocol, port) + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..81c9c31 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import pytest +from .base import Conf + + +@pytest.fixture +def conf(): + return Conf() diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/test_client.py b/tests/core/test_client.py new file mode 100644 index 0000000..4dcb704 --- /dev/null +++ b/tests/core/test_client.py @@ -0,0 +1,337 @@ +# +# This file is part of Python-AD. Python-AD is free software that is made +# available under the MIT license. Consult the file "LICENSE" that is +# distributed together with this file for the exact licensing terms. +# +# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file +# "AUTHORS" for a complete overview. + +from __future__ import absolute_import +import pytest + +from activedirectory.core.object import activate +from activedirectory.core.client import Client +from activedirectory.core.locate import Locator +from activedirectory.core.constant import AD_USERCTRL_NORMAL_ACCOUNT +from activedirectory.core.creds import Creds +from activedirectory.core.exception import Error as ADError +from six.moves import range + +from ..base import assert_raises +from . import utils + + +class TestADClient(object): + """Test suite for ADClient""" + + def test_search(self, conf): + pytest.skip('test disabled: hanging') + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + result = client.search('(objectClass=user)') + assert len(result) > 1 + + def test_add(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + user = utils.create_user(client, 'test-usr') + delete_obj(client, user) + + def test_delete(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + dn = utils.create_user(client, 'test-usr') + client.delete(dn) + + def test_modify(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + user = utils.create_user(client, 'test-usr') + mods = [] + mods.append(('replace', 'sAMAccountName', ['test-usr-2'])) + client.modify(user, mods) + delete_obj(client, user) + + def test_modrdn(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') + if result: + client.delete(result[0][0]) + user = utils.create_user(client, 'test-usr') + client.modrdn(user, 'cn=test-usr2') + result = client.search('(&(objectClass=user)(cn=test-usr2))') + assert len(result) == 1 + + def test_rename(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') + if result: + client.delete(result[0][0]) + user = utils.create_user(client, 'test-usr') + client.rename(user, 'cn=test-usr2') + result = client.search('(&(objectClass=user)(cn=test-usr2))') + assert len(result) == 1 + user = result[0][0] + ou = utils.create_ou(client, 'test-ou') + client.rename(user, 'cn=test-usr', ou) + newdn = 'cn=test-usr,%s' % ou + result = client.search('(&(objectClass=user)(cn=test-usr))') + assert len(result) == 1 + assert result[0][0].lower() == newdn.lower() + + def test_forest(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + forest = client.forest() + assert forest + assert forest.isupper() + + def test_domains(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + domains = client.domains() + for domain in domains: + assert domain + assert domain.isupper() + + def test_naming_contexts(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + naming_contexts = client.naming_contexts() + assert len(naming_contexts) >= 3 + + def test_search_all_domains(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + domains = client.domains() + for domain in domains: + base = client.dn_from_domain_name(domain) + result = client.search('(objectClass=*)', base=base, scope='base') + assert len(result) == 1 + + def test_search_schema(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + base = client.schema_base() + result = client.search('(objectClass=*)', base=base, scope='base') + assert len(result) == 1 + + def test_search_configuration(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + base = client.configuration_base() + result = client.search('(objectClass=*)', base=base, scope='base') + assert len(result) == 1 + + def test_incremental_retrieval_of_multivalued_attributes(self, conf): + conf.require(ad_admin=True, expensive=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + user = utils.create_user(client, 'test-usr') + groups = [] + for i in range(2000): + group = utils.create_group(client, 'test-grp-%04d' % i) + utils.add_user_to_group(client, user, group) + groups.append(group) + result = client.search('(sAMAccountName=test-usr)') + assert len(result) == 1 + dn, attrs = result[0] + assert 'memberOf' in attrs + assert len(attrs['memberOf']) == 2000 + delete_obj(client, user) + for group in groups: + utils.delete_group(client, group) + + def test_paged_results(self, conf): + conf.require(ad_admin=True, expensive=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + users = [] + for i in range(2000): + user = utils.create_user(client, 'test-usr-%04d' % i) + users.append(user) + result = client.search('(cn=test-usr-*)') + assert len(result) == 2000 + for user in users: + delete_obj(client, user) + + def test_search_rootdse(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + locator = Locator() + server = locator.locate(domain) + client = Client(domain) + result = client.search(base='', scope='base', server=server) + assert len(result) == 1 + dns, attrs = result[0] + assert 'supportedControl' in attrs + assert 'supportedSASLMechanisms' in attrs + + def test_search_server(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + locator = Locator() + server = locator.locate(domain) + client = Client(domain) + result = client.search('(objectClass=user)', server=server) + assert len(result) > 1 + + def test_search_gc(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) + activate(creds) + client = Client(domain) + result = client.search('(objectClass=user)', scheme='gc') + assert len(result) > 1 + for res in result: + dn, attrs = res + # accountExpires is always set, but is not a GC attribute + assert 'accountExpires' not in attrs + + def test_set_password(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + user = utils.create_user(client, 'test-usr-1') + principal = 'test-usr-1@%s' % domain + client.set_password(principal, 'Pass123') + mods = [] + ctrl = AD_USERCTRL_NORMAL_ACCOUNT + mods.append(('replace', 'userAccountControl', [str(ctrl)])) + client.modify(user, mods) + creds = Creds(domain) + creds.acquire('test-usr-1', 'Pass123') + assert_raises(ADError, creds.acquire, 'test-usr-1', 'Pass321') + delete_obj(client, user) + + def test_set_password_target_pdc(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + locator = Locator() + pdc = locator.locate(domain, role='pdc') + user = utils.create_user(client, 'test-usr-2', server=pdc) + principal = 'test-usr-2@%s' % domain + client.set_password(principal, 'Pass123', server=pdc) + mods = [] + ctrl = AD_USERCTRL_NORMAL_ACCOUNT + mods.append(('replace', 'userAccountControl', [str(ctrl)])) + client.modify(user, mods, server=pdc) + creds = Creds(domain) + creds.acquire('test-usr-2', 'Pass123', server=pdc) + assert_raises(ADError, creds.acquire, 'test-usr-2','Pass321', server=pdc) + delete_obj(client, user, server=pdc) + + def test_change_password(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + user = utils.create_user(client, 'test-usr-3') + principal = 'test-usr-3@%s' % domain + client.set_password(principal, 'Pass123') + mods = [] + ctrl = AD_USERCTRL_NORMAL_ACCOUNT + mods.append(('replace', 'userAccountControl', [str(ctrl)])) + mods.append(('replace', 'pwdLastSet', ['0'])) + client.modify(user, mods) + client.change_password(principal, 'Pass123', 'Pass456') + creds = Creds(domain) + creds.acquire('test-usr-3', 'Pass456') + assert_raises(ADError, creds.acquire, 'test-usr-3', 'Pass321') + delete_obj(client, user) + + def test_change_password_target_pdc(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() + creds = Creds(domain) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) + activate(creds) + client = Client(domain) + locator = Locator() + pdc = locator.locate(domain, role='pdc') + user = utils.create_user(client, 'test-usr-4', server=pdc) + principal = 'test-usr-4@%s' % domain + client.set_password(principal, 'Pass123', server=pdc) + mods = [] + ctrl = AD_USERCTRL_NORMAL_ACCOUNT + mods.append(('replace', 'userAccountControl', [str(ctrl)])) + mods.append(('replace', 'pwdLastSet', ['0'])) + client.modify(user, mods, server=pdc) + client.change_password(principal, 'Pass123', 'Pass456', server=pdc) + creds = Creds(domain) + creds.acquire('test-usr-4', 'Pass456', server=pdc) + assert_raises(ADError, creds.acquire, 'test-usr-4', 'Pass321', server=pdc) + delete_obj(client, user, server=pdc) diff --git a/lib/ad/core/test/test_creds.py b/tests/core/test_creds.py similarity index 71% rename from lib/ad/core/test/test_creds.py rename to tests/core/test_creds.py index 02a3efc..c3d78ad 100644 --- a/lib/ad/core/test/test_creds.py +++ b/tests/core/test_creds.py @@ -6,23 +6,23 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import os import pexpect -from ad.test.base import BaseTest -from ad.core.creds import Creds as ADCreds -from ad.core.object import instance, activate +from activedirectory.core.creds import Creds as ADCreds +from activedirectory.core.object import instance, activate -class TestCreds(BaseTest): - """Test suite for ad.core.creds.""" +class TestCreds(object): + """Test suite for activedirectory.core.creds.""" - def test_acquire_password(self): - self.require(ad_user=True) - domain = self.domain() + def test_acquire_password(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = ADCreds(domain) - principal = self.ad_user_account() - password = self.ad_user_password() + principal = conf.ad_user_account() + password = conf.ad_user_password() creds.acquire(principal, password) principal = '%s@%s' % (principal, domain) assert creds.principal().lower() == principal.lower() @@ -30,12 +30,12 @@ def test_acquire_password(self): pattern = '.*krbtgt/%s@%s' % (domain.upper(), domain.upper()) assert child.expect([pattern]) == 0 - def test_acquire_keytab(self): - self.require(ad_user=True) - domain = self.domain() + def test_acquire_keytab(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = ADCreds(domain) - principal = self.ad_user_account() - password = self.ad_user_password() + principal = conf.ad_user_account() + password = conf.ad_user_password() creds.acquire(principal, password) os.environ['PATH'] = '/usr/kerberos/sbin:/usr/kerberos/bin:%s' % \ os.environ['PATH'] @@ -51,7 +51,7 @@ def test_acquire_keytab(self): child.expect('Password for.*:') child.sendline(password) child.expect('ktutil:') - keytab = self.tempfile(remove=True) + keytab = conf.tempfile(remove=True) child.sendline('wkt %s' % keytab) child.expect('ktutil:') child.sendline('quit') @@ -62,24 +62,24 @@ def test_acquire_keytab(self): pattern = '.*krbtgt/%s@%s' % (domain.upper(), domain.upper()) assert child.expect([pattern]) == 0 - def test_load(self): - self.require(ad_user=True) - domain = self.domain().upper() - principal = '%s@%s' % (self.ad_user_account(), domain) - self.acquire_credentials(principal, self.ad_user_password()) + def test_load(self, conf): + conf.require(ad_user=True) + domain = conf.domain().upper() + principal = '%s@%s' % (conf.ad_user_account(), domain) + conf.acquire_credentials(principal, conf.ad_user_password()) creds = ADCreds(domain) creds.load() assert creds.principal().lower() == principal.lower() - ccache, princ, creds = self.list_credentials() + ccache, princ, creds = conf.list_credentials() assert princ.lower() == principal.lower() assert len(creds) > 0 assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) - def test_acquire_multi(self): - self.require(ad_user=True) - domain = self.domain() - principal = self.ad_user_account() - password = self.ad_user_password() + def test_acquire_multi(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + principal = conf.ad_user_account() + password = conf.ad_user_password() creds1 = ADCreds(domain) creds1.acquire(principal, password) ccache1 = creds1._ccache_name() @@ -101,11 +101,11 @@ def test_acquire_multi(self): assert os.environ['KRB5CCNAME'] == ccache2 assert os.environ['KRB5_CONFIG'] == config2 - def test_release_multi(self): - self.require(ad_user=True) - domain = self.domain() - principal = self.ad_user_account() - password = self.ad_user_password() + def test_release_multi(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + principal = conf.ad_user_account() + password = conf.ad_user_password() ccorig = os.environ.get('KRB5CCNAME') cforig = os.environ.get('KRB5_CONFIG') creds1 = ADCreds(domain) @@ -123,11 +123,11 @@ def test_release_multi(self): assert os.environ.get('KRB5CCNAME') == ccorig assert os.environ.get('KRB5_CONFIG') == cforig - def test_cleanup_files(self): - self.require(ad_user=True) - domain = self.domain() - principal = self.ad_user_account() - password = self.ad_user_password() + def test_cleanup_files(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + principal = conf.ad_user_account() + password = conf.ad_user_password() creds = ADCreds(domain) creds.acquire(principal, password) ccache = creds._ccache_name() @@ -138,11 +138,11 @@ def test_cleanup_files(self): assert not os.access(ccache, os.R_OK) assert not os.access(config, os.R_OK) - def test_cleanup_environment(self): - self.require(ad_user=True) - domain = self.domain() - principal = self.ad_user_account() - password = self.ad_user_password() + def test_cleanup_environment(self, conf): + conf.require(ad_user=True) + domain = conf.domain() + principal = conf.ad_user_account() + password = conf.ad_user_password() ccorig = os.environ.get('KRB5CCNAME') cforig = os.environ.get('KRB5_CONFIG') creds = ADCreds(domain) diff --git a/lib/ad/core/test/test_locate.py b/tests/core/test_locate.py similarity index 74% rename from lib/ad/core/test/test_locate.py rename to tests/core/test_locate.py index bd4d2a7..ec89713 100644 --- a/lib/ad/core/test/test_locate.py +++ b/tests/core/test_locate.py @@ -6,12 +6,14 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import +from __future__ import print_function import math import signal -from ad.test.base import BaseTest -from ad.core.locate import Locator +from activedirectory.core.locate import Locator from threading import Timer +from six.moves import range class SRV(object): @@ -24,12 +26,12 @@ def __init__(self, priority=0, weight=100, target=None, port=None): self.port = port -class TestLocator(BaseTest): +class TestLocator(object): """Test suite for Locator.""" - def test_simple(self): - self.require(ad_user=True) - domain = self.domain() + def test_simple(self, conf): + conf.require(ad_user=True) + domain = conf.domain() loc = Locator() result = loc.locate_many(domain) assert len(result) > 0 @@ -38,17 +40,17 @@ def test_simple(self): result = loc.locate_many(domain, role='pdc') assert len(result) == 1 - def test_network_failure(self): - self.require(ad_user=True, local_admin=True, firewall=True) - domain = self.domain() + def test_network_failure(self, conf): + conf.require(ad_user=True, local_admin=True, firewall=True) + domain = conf.domain() loc = Locator() # Block outgoing DNS and CLDAP traffic and enable it after 3 seconds. # Locator should be able to handle this. - self.remove_network_blocks() - self.block_outgoing_traffic('tcp', 53) - self.block_outgoing_traffic('udp', 53) - self.block_outgoing_traffic('udp', 389) - t = Timer(3, self.remove_network_blocks); t.start() + conf.remove_network_blocks() + conf.block_outgoing_traffic('tcp', 53) + conf.block_outgoing_traffic('udp', 53) + conf.block_outgoing_traffic('udp', 389) + t = Timer(3, conf.remove_network_blocks); t.start() result = loc.locate_many(domain) assert len(result) > 0 @@ -73,7 +75,7 @@ def test_order_dns_srv_weight(self): for i in range(n): res = loc._order_dns_srv(srv) count[res[0].weight] += 1 - print count + print(count) def stddev(n, p): # standard deviation of binomial distribution @@ -85,9 +87,9 @@ def stddev(n, p): # asserting an error here. assert abs(count[x] - n*p) < 6 * stddev(n, p) - def test_detect_site(self): - self.require(ad_user=True) + def test_detect_site(self, conf): + conf.require(ad_user=True) loc = Locator() - domain = self.domain() + domain = conf.domain() site = loc._detect_site(domain) assert site is not None diff --git a/tests/core/utils.py b/tests/core/utils.py new file mode 100644 index 0000000..c283759 --- /dev/null +++ b/tests/core/utils.py @@ -0,0 +1,72 @@ +from activedirectory.core.exception import Error as ADError, LDAPError +from activedirectory.core.constant import ( + AD_USERCTRL_ACCOUNT_DISABLED, + AD_USERCTRL_NORMAL_ACCOUNT +) + + +def delete_obj(client, dn, server=None): + try: + client.delete(dn, server=server) + except (ADError, LDAPError): + pass + +def delete_user(client, name, server=None): + # Delete any user that may conflict with a newly to be created user + filter = '(|(cn=%s)(sAMAccountName=%s)(userPrincipalName=%s))' % \ + (name, name, '%s@%s' % (name, client.domain().upper())) + result = client.search('(&(objectClass=user)(sAMAccountName=%s))' % name, + server=server) + for res in result: + client.delete(res[0], server=server) + + +def create_user(client, name, server=None): + attrs = [] + attrs.append(('cn', [name])) + attrs.append(('sAMAccountName', [name])) + attrs.append(('userPrincipalName', ['%s@%s' % (name, client.domain().upper())])) + ctrl = AD_USERCTRL_ACCOUNT_DISABLED | AD_USERCTRL_NORMAL_ACCOUNT + attrs.append(('userAccountControl', [str(ctrl)])) + attrs.append(('objectClass', ['user'])) + dn = 'cn=%s,cn=users,%s' % (name, client.dn_from_domain_name(client.domain())) + delete_user(client, name, server=server) + client.add(dn, attrs, server=server) + return dn + + +def create_ou(client, name, server=None): + attrs = [] + attrs.append(('objectClass', ['organizationalUnit'])) + attrs.append(('ou', [name])) + dn = 'ou=%s,%s' % (name, client.dn_from_domain_name(client.domain())) + delete_obj(client, dn, server=server) + client.add(dn, attrs, server=server) + return dn + +def delete_group(client, dn, server=None): + try: + client.delete(dn, server=server) + except (ADError, LDAPError): + pass + +def create_group(client, name, server=None): + attrs = [] + attrs.append(('cn', [name])) + attrs.append(('sAMAccountName', [name])) + attrs.append(('objectClass', ['group'])) + dn = 'cn=%s,cn=Users,%s' % (name, client.dn_from_domain_name(client.domain())) + delete_group(client, dn, server=server) + client.add(dn, attrs, server=server) + return dn + +def add_user_to_group(client, user, group): + mods = [] + mods.append(('delete', 'member', [user])) + try: + client.modify(group, mods) + except (ADError, LDAPError): + pass + mods = [] + mods.append(('add', 'member', [user])) + client.modify(group, mods) diff --git a/tests/protocol/__init__.py b/tests/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/ad/protocol/test/netlogon.bin b/tests/protocol/netlogon.bin similarity index 100% rename from lib/ad/protocol/test/netlogon.bin rename to tests/protocol/netlogon.bin diff --git a/lib/ad/protocol/test/searchrequest.bin b/tests/protocol/searchrequest.bin similarity index 100% rename from lib/ad/protocol/test/searchrequest.bin rename to tests/protocol/searchrequest.bin diff --git a/lib/ad/protocol/test/searchresult.bin b/tests/protocol/searchresult.bin similarity index 100% rename from lib/ad/protocol/test/searchresult.bin rename to tests/protocol/searchresult.bin diff --git a/lib/ad/protocol/test/test_asn1.py b/tests/protocol/test_asn1.py similarity index 78% rename from lib/ad/protocol/test/test_asn1.py rename to tests/protocol/test_asn1.py index a5a7cfe..891df1b 100644 --- a/lib/ad/protocol/test/test_asn1.py +++ b/tests/protocol/test_asn1.py @@ -6,8 +6,11 @@ # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the # file "AUTHORS" for a complete overview. -from ad.protocol import asn1 -from nose.tools import assert_raises +from __future__ import absolute_import +from activedirectory.protocol import asn1 +import six + +from ..base import assert_raises class TestEncoder(object): @@ -18,104 +21,104 @@ def test_boolean(self): enc.start() enc.write(True, asn1.Boolean) res = enc.output() - assert res == '\x01\x01\xff' + assert res == b'\x01\x01\xff' def test_integer(self): enc = asn1.Encoder() enc.start() enc.write(1) res = enc.output() - assert res == '\x02\x01\x01' + assert res == b'\x02\x01\x01' def test_long_integer(self): enc = asn1.Encoder() enc.start() - enc.write(0x0102030405060708090a0b0c0d0e0fL) + enc.write(0x0102030405060708090a0b0c0d0e0f) res = enc.output() - assert res == '\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + assert res == b'\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' def test_negative_integer(self): enc = asn1.Encoder() enc.start() enc.write(-1) res = enc.output() - assert res == '\x02\x01\xff' + assert res == b'\x02\x01\xff' def test_long_negative_integer(self): enc = asn1.Encoder() enc.start() - enc.write(-0x0102030405060708090a0b0c0d0e0fL) + enc.write(-0x0102030405060708090a0b0c0d0e0f) res = enc.output() - assert res == '\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' + assert res == b'\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' def test_twos_complement_boundaries(self): enc = asn1.Encoder() enc.start() enc.write(127) res = enc.output() - assert res == '\x02\x01\x7f' + assert res == b'\x02\x01\x7f' enc.start() enc.write(128) res = enc.output() - assert res == '\x02\x02\x00\x80' + assert res == b'\x02\x02\x00\x80' enc.start() enc.write(-128) res = enc.output() - assert res == '\x02\x01\x80' + assert res == b'\x02\x01\x80' enc.start() enc.write(-129) res = enc.output() - assert res == '\x02\x02\xff\x7f' + assert res == b'\x02\x02\xff\x7f' def test_octet_string(self): enc = asn1.Encoder() enc.start() enc.write('foo') res = enc.output() - assert res == '\x04\x03foo' + assert res == b'\x04\x03foo' def test_null(self): enc = asn1.Encoder() enc.start() enc.write(None) res = enc.output() - assert res == '\x05\x00' + assert res == b'\x05\x00' def test_object_identifier(self): enc = asn1.Encoder() enc.start() enc.write('1.2.3', asn1.ObjectIdentifier) res = enc.output() - assert res == '\x06\x02\x2a\x03' + assert res == b'\x06\x02\x2a\x03' def test_long_object_identifier(self): enc = asn1.Encoder() enc.start() enc.write('39.2.3', asn1.ObjectIdentifier) res = enc.output() - assert res == '\x06\x03\x8c\x1a\x03' + assert res == b'\x06\x03\x8c\x1a\x03' enc.start() enc.write('1.39.3', asn1.ObjectIdentifier) res = enc.output() - assert res == '\x06\x02\x4f\x03' + assert res == b'\x06\x02\x4f\x03' enc.start() enc.write('1.2.300000', asn1.ObjectIdentifier) res = enc.output() - assert res == '\x06\x04\x2a\x92\xa7\x60' + assert res == b'\x06\x04\x2a\x92\xa7\x60' def test_real_object_identifier(self): enc = asn1.Encoder() enc.start() enc.write('1.2.840.113554.1.2.1.1', asn1.ObjectIdentifier) res = enc.output() - assert res == '\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' + assert res == b'\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' def test_enumerated(self): enc = asn1.Encoder() enc.start() enc.write(1, asn1.Enumerated) res = enc.output() - assert res == '\x0a\x01\x01' + assert res == b'\x0a\x01\x01' def test_sequence(self): enc = asn1.Encoder() @@ -125,7 +128,7 @@ def test_sequence(self): enc.write('foo') enc.leave() res = enc.output() - assert res == '\x30\x08\x02\x01\x01\x04\x03foo' + assert res == b'\x30\x08\x02\x01\x01\x04\x03foo' def test_sequence_of(self): enc = asn1.Encoder() @@ -135,7 +138,7 @@ def test_sequence_of(self): enc.write(2) enc.leave() res = enc.output() - assert res == '\x30\x06\x02\x01\x01\x02\x01\x02' + assert res == b'\x30\x06\x02\x01\x01\x02\x01\x02' def test_set(self): enc = asn1.Encoder() @@ -145,7 +148,7 @@ def test_set(self): enc.write('foo') enc.leave() res = enc.output() - assert res == '\x31\x08\x02\x01\x01\x04\x03foo' + assert res == b'\x31\x08\x02\x01\x01\x04\x03foo' def test_set_of(self): enc = asn1.Encoder() @@ -155,7 +158,7 @@ def test_set_of(self): enc.write(2) enc.leave() res = enc.output() - assert res == '\x31\x06\x02\x01\x01\x02\x01\x02' + assert res == b'\x31\x06\x02\x01\x01\x02\x01\x02' def test_context(self): enc = asn1.Encoder() @@ -164,7 +167,7 @@ def test_context(self): enc.write(1) enc.leave() res = enc.output() - assert res == '\xa1\x03\x02\x01\x01' + assert res == b'\xa1\x03\x02\x01\x01' def test_application(self): enc = asn1.Encoder() @@ -173,7 +176,7 @@ def test_application(self): enc.write(1) enc.leave() res = enc.output() - assert res == '\x61\x03\x02\x01\x01' + assert res == b'\x61\x03\x02\x01\x01' def test_private(self): enc = asn1.Encoder() @@ -182,7 +185,7 @@ def test_private(self): enc.write(1) enc.leave() res = enc.output() - assert res == '\xe1\x03\x02\x01\x01' + assert res == b'\xe1\x03\x02\x01\x01' def test_long_tag_id(self): enc = asn1.Encoder() @@ -191,14 +194,14 @@ def test_long_tag_id(self): enc.write(1) enc.leave() res = enc.output() - assert res == '\x3f\x83\xff\x7f\x03\x02\x01\x01' + assert res == b'\x3f\x83\xff\x7f\x03\x02\x01\x01' def test_long_tag_length(self): enc = asn1.Encoder() enc.start() enc.write('x' * 0xffff) res = enc.output() - assert res == '\x04\x82\xff\xff' + 'x' * 0xffff + assert res == b'\x04\x82\xff\xff' + b'x' * 0xffff def test_error_init(self): enc = asn1.Encoder() @@ -232,7 +235,7 @@ class TestDecoder(object): """Test suite for ASN1 Decoder.""" def test_boolean(self): - buf = '\x01\x01\xff' + buf = b'\x01\x01\xff' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -240,19 +243,19 @@ def test_boolean(self): tag, val = dec.read() assert isinstance(val, int) assert val == True - buf = '\x01\x01\x01' + buf = b'\x01\x01\x01' dec.start(buf) tag, val = dec.read() assert isinstance(val, int) assert val == True - buf = '\x01\x01\x00' + buf = b'\x01\x01\x00' dec.start(buf) tag, val = dec.read() assert isinstance(val, int) assert val == False def test_integer(self): - buf = '\x02\x01\x01' + buf = b'\x02\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -262,57 +265,57 @@ def test_integer(self): assert val == 1 def test_long_integer(self): - buf = '\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + buf = b'\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() - assert val == 0x0102030405060708090a0b0c0d0e0fL + assert val == 0x0102030405060708090a0b0c0d0e0f def test_negative_integer(self): - buf = '\x02\x01\xff' + buf = b'\x02\x01\xff' dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() assert val == -1 def test_long_negative_integer(self): - buf = '\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' + buf = b'\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() - assert val == -0x0102030405060708090a0b0c0d0e0fL + assert val == -0x0102030405060708090a0b0c0d0e0f def test_twos_complement_boundaries(self): - buf = '\x02\x01\x7f' + buf = b'\x02\x01\x7f' dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() assert val == 127 - buf = '\x02\x02\x00\x80' + buf = b'\x02\x02\x00\x80' dec.start(buf) tag, val = dec.read() assert val == 128 - buf = '\x02\x01\x80' + buf = b'\x02\x01\x80' dec.start(buf) tag, val = dec.read() assert val == -128 - buf = '\x02\x02\xff\x7f' + buf = b'\x02\x02\xff\x7f' dec.start(buf) tag, val = dec.read() assert val == -129 def test_octet_string(self): - buf = '\x04\x03foo' + buf = b'\x04\x03foo' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() assert tag == (asn1.OctetString, asn1.TypePrimitive, asn1.ClassUniversal) tag, val = dec.read() - assert isinstance(val, str) - assert val == 'foo' + assert isinstance(val, six.binary_type) + assert val == b'foo' def test_null(self): - buf = '\x05\x00' + buf = b'\x05\x00' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -322,38 +325,38 @@ def test_null(self): def test_object_identifier(self): dec = asn1.Decoder() - buf = '\x06\x02\x2a\x03' + buf = b'\x06\x02\x2a\x03' dec.start(buf) tag = dec.peek() assert tag == (asn1.ObjectIdentifier, asn1.TypePrimitive, asn1.ClassUniversal) tag, val = dec.read() - assert val == '1.2.3' + assert val == b'1.2.3' def test_long_object_identifier(self): dec = asn1.Decoder() - buf = '\x06\x03\x8c\x1a\x03' + buf = b'\x06\x03\x8c\x1a\x03' dec.start(buf) tag, val = dec.read() - assert val == '39.2.3' - buf = '\x06\x02\x4f\x03' + assert val == b'39.2.3' + buf = b'\x06\x02\x4f\x03' dec.start(buf) tag, val = dec.read() - assert val == '1.39.3' - buf = '\x06\x04\x2a\x92\xa7\x60' + assert val == b'1.39.3' + buf = b'\x06\x04\x2a\x92\xa7\x60' dec.start(buf) tag, val = dec.read() - assert val == '1.2.300000' + assert val == b'1.2.300000' def test_real_object_identifier(self): dec = asn1.Decoder() - buf = '\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' + buf = b'\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' dec.start(buf) tag, val = dec.read() - assert val == '1.2.840.113554.1.2.1.1' + assert val == b'1.2.840.113554.1.2.1.1' def test_enumerated(self): - buf = '\x0a\x01\x01' + buf = b'\x0a\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -363,7 +366,7 @@ def test_enumerated(self): assert val == 1 def test_sequence(self): - buf = '\x30\x08\x02\x01\x01\x04\x03foo' + buf = b'\x30\x08\x02\x01\x01\x04\x03foo' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -372,10 +375,10 @@ def test_sequence(self): tag, val = dec.read() assert val == 1 tag, val = dec.read() - assert val == 'foo' + assert val == b'foo' def test_sequence_of(self): - buf = '\x30\x06\x02\x01\x01\x02\x01\x02' + buf = b'\x30\x06\x02\x01\x01\x02\x01\x02' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -387,7 +390,7 @@ def test_sequence_of(self): assert val == 2 def test_set(self): - buf = '\x31\x08\x02\x01\x01\x04\x03foo' + buf = b'\x31\x08\x02\x01\x01\x04\x03foo' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -396,10 +399,10 @@ def test_set(self): tag, val = dec.read() assert val == 1 tag, val = dec.read() - assert val == 'foo' + assert val == b'foo' def test_set_of(self): - buf = '\x31\x06\x02\x01\x01\x02\x01\x02' + buf = b'\x31\x06\x02\x01\x01\x02\x01\x02' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -411,7 +414,7 @@ def test_set_of(self): assert val == 2 def test_context(self): - buf = '\xa1\x03\x02\x01\x01' + buf = b'\xa1\x03\x02\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -421,7 +424,7 @@ def test_context(self): assert val == 1 def test_application(self): - buf = '\x61\x03\x02\x01\x01' + buf = b'\x61\x03\x02\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -431,7 +434,7 @@ def test_application(self): assert val == 1 def test_private(self): - buf = '\xe1\x03\x02\x01\x01' + buf = b'\xe1\x03\x02\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -441,7 +444,7 @@ def test_private(self): assert val == 1 def test_long_tag_id(self): - buf = '\x3f\x83\xff\x7f\x03\x02\x01\x01' + buf = b'\x3f\x83\xff\x7f\x03\x02\x01\x01' dec = asn1.Decoder() dec.start(buf) tag = dec.peek() @@ -451,14 +454,14 @@ def test_long_tag_id(self): assert val == 1 def test_long_tag_length(self): - buf = '\x04\x82\xff\xff' + 'x' * 0xffff + buf = b'\x04\x82\xff\xff' + b'x' * 0xffff dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() - assert val == 'x' * 0xffff + assert val == b'x' * 0xffff def test_read_multiple(self): - buf = '\x02\x01\x01\x02\x01\x02' + buf = b'\x02\x01\x01\x02\x01\x02' dec = asn1.Decoder() dec.start(buf) tag, val = dec.read() @@ -468,7 +471,7 @@ def test_read_multiple(self): assert dec.eof() def test_skip_primitive(self): - buf = '\x02\x01\x01\x02\x01\x02' + buf = b'\x02\x01\x01\x02\x01\x02' dec = asn1.Decoder() dec.start(buf) dec.read() @@ -477,7 +480,7 @@ def test_skip_primitive(self): assert dec.eof() def test_skip_constructed(self): - buf = '\x30\x06\x02\x01\x01\x02\x01\x02\x02\x01\x03' + buf = b'\x30\x06\x02\x01\x01\x02\x01\x02\x02\x01\x03' dec = asn1.Decoder() dec.start(buf) dec.read() @@ -493,7 +496,7 @@ def test_error_init(self): assert_raises(asn1.Error, dec.leave) def test_error_stack(self): - buf = '\x30\x08\x02\x01\x01\x04\x03foo' + buf = b'\x30\x08\x02\x01\x01\x04\x03foo' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.leave) @@ -503,69 +506,69 @@ def test_error_stack(self): def test_no_input(self): dec = asn1.Decoder() - dec.start('') + dec.start(b'') tag = dec.peek() assert tag is None def test_error_missing_tag_bytes(self): - buf = '\x3f' + buf = b'\x3f' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.peek) - buf = '\x3f\x83' + buf = b'\x3f\x83' dec.start(buf) assert_raises(asn1.Error, dec.peek) def test_error_no_length_bytes(self): - buf = '\x02' + buf = b'\x02' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_missing_length_bytes(self): - buf = '\x04\x82\xff' + buf = b'\x04\x82\xff' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_too_many_length_bytes(self): - buf = '\x04\xff' + '\xff' * 0x7f + buf = b'\x04\xff' + b'\xff' * 0x7f dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_no_value_bytes(self): - buf = '\x02\x01' + buf = b'\x02\x01' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_missing_value_bytes(self): - buf = '\x02\x02\x01' + buf = b'\x02\x02\x01' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_non_normalized_positive_integer(self): - buf = '\x02\x02\x00\x01' + buf = b'\x02\x02\x00\x01' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_non_normalized_negative_integer(self): - buf = '\x02\x02\xff\x80' + buf = b'\x02\x02\xff\x80' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_non_normalised_object_identifier(self): - buf = '\x06\x02\x80\x01' + buf = b'\x06\x02\x80\x01' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) def test_error_object_identifier_with_too_large_first_component(self): - buf = '\x06\x02\x8c\x40' + buf = b'\x06\x02\x8c\x40' dec = asn1.Decoder() dec.start(buf) assert_raises(asn1.Error, dec.read) diff --git a/tests/protocol/test_krb5.py b/tests/protocol/test_krb5.py new file mode 100644 index 0000000..1cd889b --- /dev/null +++ b/tests/protocol/test_krb5.py @@ -0,0 +1,54 @@ +# +# This file is part of Python-AD. Python-AD is free software that is made +# available under the MIT license. Consult the file "LICENSE" that is +# distributed together with this file for the exact licensing terms. +# +# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file +# "AUTHORS" for a complete overview. +"""Test suite for protocol.krb5.""" + +from __future__ import absolute_import +import os +import stat +import pexpect + +from activedirectory.protocol import krb5 +from ..base import assert_raises, Error + + +def test_cc_default(conf): + conf.require(ad_user=True) + domain = conf.domain().upper() + principal = '%s@%s' % (conf.ad_user_account(), domain) + password = conf.ad_user_password() + conf.acquire_credentials(principal, password) + ccache = krb5.cc_default() + ccname, princ, creds = conf.list_credentials(ccache) + assert princ.lower() == principal.lower() + assert len(creds) > 0 + assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) + +def test_cc_copy_creds(conf): + conf.require(ad_user=True) + domain = conf.domain().upper() + principal = '%s@%s' % (conf.ad_user_account(), domain) + password = conf.ad_user_password() + conf.acquire_credentials(principal, password) + ccache = krb5.cc_default() + cctmp = conf.tempfile() + assert_raises(Error, conf.list_credentials, cctmp) + krb5.cc_copy_creds(ccache, cctmp) + ccname, princ, creds = conf.list_credentials(cctmp) + assert princ.lower() == principal.lower() + assert len(creds) > 0 + assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) + +def test_cc_get_principal(conf): + conf.require(ad_user=True) + domain = conf.domain().upper() + principal = '%s@%s' % (conf.ad_user_account(), domain) + password = conf.ad_user_password() + conf.acquire_credentials(principal, password) + ccache = krb5.cc_default() + princ = krb5.cc_get_principal(ccache) + assert princ.lower() == principal.lower() diff --git a/tests/protocol/test_ldap.py b/tests/protocol/test_ldap.py new file mode 100644 index 0000000..c9fdda5 --- /dev/null +++ b/tests/protocol/test_ldap.py @@ -0,0 +1,39 @@ +# +# This file is part of Python-AD. Python-AD is free software that is made +# available under the MIT license. Consult the file "LICENSE" that is +# distributed together with this file for the exact licensing terms. +# +# Python-AD is copyright (c) 2007 by the Python-AD authors. See the file +# "AUTHORS" for a complete overview. +"""Test suite for activedirectory.util.ldap.""" + +from __future__ import absolute_import +import os.path +from activedirectory.protocol import ldap + + + +def test_encode_real_search_request(conf): + client = ldap.Client() + filter = '(&(DnsDomain=FREEADI.ORG)(Host=magellan)(NtVer=\\06\\00\\00\\00))' + req = client.create_search_request('', filter, ('NetLogon',), + scope=ldap.SCOPE_BASE, msgid=4) + + buf = conf.read_file('protocol/searchrequest.bin') + assert req == buf + +def test_decode_real_search_reply(conf): + client = ldap.Client() + buf = conf.read_file('protocol/searchresult.bin') + reply = client.parse_message_header(buf) + assert reply == (4, 4) + reply = client.parse_search_result(buf) + assert len(reply) == 1 + msgid, dn, attrs = reply[0] + assert msgid == 4 + assert dn == b'' + + netlogon = conf.read_file('protocol/netlogon.bin') + print(repr(attrs)) + print(repr({ 'netlogon': [netlogon] })) + assert attrs == { b'netlogon': [netlogon] } diff --git a/lib/ad/protocol/test/test_ldapfilter.py b/tests/protocol/test_ldapfilter.py similarity index 96% rename from lib/ad/protocol/test/test_ldapfilter.py rename to tests/protocol/test_ldapfilter.py index ce33ff9..6cb2e25 100644 --- a/lib/ad/protocol/test/test_ldapfilter.py +++ b/tests/protocol/test_ldapfilter.py @@ -6,12 +6,14 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. -from nose.tools import assert_raises -from ad.protocol import ldapfilter +from __future__ import absolute_import +from activedirectory.protocol import ldapfilter + +from ..base import assert_raises class TestLDAPFilterParser(object): - """Test suite for ad.protocol.ldapfilter.""" + """Test suite for activedirectory.protocol.ldapfilter.""" def test_equals(self): filt = '(type=value)' diff --git a/lib/ad/protocol/test/test_netlogon.py b/tests/protocol/test_netlogon.py similarity index 51% rename from lib/ad/protocol/test/test_netlogon.py rename to tests/protocol/test_netlogon.py index 4e8cef4..a95cfcf 100644 --- a/lib/ad/protocol/test/test_netlogon.py +++ b/tests/protocol/test_netlogon.py @@ -6,147 +6,154 @@ # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. +from __future__ import absolute_import import os.path import signal import dns.resolver - from threading import Timer -from nose.tools import assert_raises -from ad.test.base import BaseTest -from ad.protocol import netlogon +import six +from six.moves import range -class TestDecoder(BaseTest): - """Test suite for netlogon.Decoder.""" +import pytest +from activedirectory.protocol import netlogon + +from ..base import assert_raises + + +def decode_uint32(buffer, offset): + d = netlogon.Decoder() + d.start(buffer) + d._set_offset(offset) + return d._decode_uint32(), d._offset() - def decode_uint32(self, buffer, offset): - d = netlogon.Decoder() - d.start(buffer) - d._set_offset(offset) - return d._decode_uint32(), d._offset() + +def decode_rfc1035(buffer, offset): + d = netlogon.Decoder() + d.start(buffer) + d._set_offset(offset) + return d._decode_rfc1035(), d._offset() + + +class TestDecoder(object): + """Test suite for netlogon.Decoder.""" def test_uint32_simple(self): - s = '\x01\x00\x00\x00' - assert self.decode_uint32(s, 0) == (1, 4) + s = b'\x01\x00\x00\x00' + assert decode_uint32(s, 0) == (1, 4) def test_uint32_byte_order(self): - s = '\x00\x01\x00\x00' - assert self.decode_uint32(s, 0) == (0x100, 4) - s = '\x00\x00\x01\x00' - assert self.decode_uint32(s, 0) == (0x10000, 4) - s = '\x00\x00\x00\x01' - assert self.decode_uint32(s, 0) == (0x1000000, 4) + s = b'\x00\x01\x00\x00' + assert decode_uint32(s, 0) == (0x100, 4) + s = b'\x00\x00\x01\x00' + assert decode_uint32(s, 0) == (0x10000, 4) + s = b'\x00\x00\x00\x01' + assert decode_uint32(s, 0) == (0x1000000, 4) def test_uint32_long(self): - s = '\x00\x00\x00\xff' - assert self.decode_uint32(s, 0) == (0xff000000L, 4) - s = '\xff\xff\xff\xff' - assert self.decode_uint32(s, 0) == (0xffffffffL, 4) + s = b'\x00\x00\x00\xff' + assert decode_uint32(s, 0) == (0xff000000, 4) + s = b'\xff\xff\xff\xff' + assert decode_uint32(s, 0) == (0xffffffff, 4) def test_error_uint32_null_input(self): - s = '' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) + s = b'' + assert_raises(netlogon.Error, decode_uint32, s, 0) def test_error_uint32_short_input(self): - s = '\x00' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) - s = '\x00\x00' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) - s = '\x00\x00\x00' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) - - def decode_rfc1035(self, buffer, offset): - d = netlogon.Decoder() - d.start(buffer) - d._set_offset(offset) - return d._decode_rfc1035(), d._offset() + s = b'\x00' + assert_raises(netlogon.Error, decode_uint32, s, 0) + s = b'\x00\x00' + assert_raises(netlogon.Error, decode_uint32, s, 0) + s = b'\x00\x00\x00' + assert_raises(netlogon.Error, decode_uint32, s, 0) def test_rfc1035_simple(self): - s = '\x03foo\x00' - assert self.decode_rfc1035(s, 0) == ('foo', 5) + s = b'\x03foo\x00' + assert decode_rfc1035(s, 0) == (b'foo', 5) def test_rfc1035_multi_component(self): - s = '\x03foo\x03bar\x00' - assert self.decode_rfc1035(s, 0) == ('foo.bar', 9) + s = b'\x03foo\x03bar\x00' + assert decode_rfc1035(s, 0) == (b'foo.bar', 9) def test_rfc1035_pointer(self): - s = '\x03foo\x00\xc0\x00' - assert self.decode_rfc1035(s, 5) == ('foo', 7) + s = b'\x03foo\x00\xc0\x00' + assert decode_rfc1035(s, 5) == (b'foo', 7) def test_rfc1035_forward_pointer(self): - s = '\xc0\x02\x03foo\x00' - assert self.decode_rfc1035(s, 0) == ('foo', 2) + s = b'\xc0\x02\x03foo\x00' + assert decode_rfc1035(s, 0) == (b'foo', 2) def test_rfc1035_pointer_component(self): - s = '\x03foo\x00\x03bar\xc0\x00' - assert self.decode_rfc1035(s, 5) == ('bar.foo', 11) + s = b'\x03foo\x00\x03bar\xc0\x00' + assert decode_rfc1035(s, 5) == (b'bar.foo', 11) def test_rfc1035_pointer_multi_component(self): - s = '\x03foo\x03bar\x00\x03baz\xc0\x00' - assert self.decode_rfc1035(s, 9) == ('baz.foo.bar', 15) + s = b'\x03foo\x03bar\x00\x03baz\xc0\x00' + assert decode_rfc1035(s, 9) == (b'baz.foo.bar', 15) def test_rfc1035_pointer_recursive(self): - s = '\x03foo\x00\x03bar\xc0\x00\x03baz\xc0\x05' - assert self.decode_rfc1035(s, 11) == ('baz.bar.foo', 17) + s = b'\x03foo\x00\x03bar\xc0\x00\x03baz\xc0\x05' + assert decode_rfc1035(s, 11) == (b'baz.bar.foo', 17) def test_rfc1035_multi_string(self): - s = '\x03foo\x00\x03bar\x00' - assert self.decode_rfc1035(s, 0) == ('foo', 5) - assert self.decode_rfc1035(s, 5) == ('bar', 10) + s = b'\x03foo\x00\x03bar\x00' + assert decode_rfc1035(s, 0) == (b'foo', 5) + assert decode_rfc1035(s, 5) == (b'bar', 10) def test_rfc1035_null(self): - s = '\x00' - assert self.decode_rfc1035(s, 0) == ('', 1) + s = b'\x00' + assert decode_rfc1035(s, 0) == (b'', 1) def test_error_rfc1035_null_input(self): - s = '' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_missing_tag(self): - s = '\x03foo' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\x03foo' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_truncated_input(self): - s = '\x04foo' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\x04foo' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_pointer_overflow(self): - s = '\xc0\x03' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\xc0\x03' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_cyclic_pointer(self): - s = '\xc0\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) - s = '\x03foo\xc0\x06\x03bar\xc0\x0c\x03baz\xc0\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\xc0\x00' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) + s = b'\x03foo\xc0\x06\x03bar\xc0\x0c\x03baz\xc0\x00' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_illegal_tags(self): - s = '\x80' + 0x80 * 'a' + '\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) - s = '\x40' + 0x40 * 'a' + '\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\x80' + 0x80 * b'a' + b'\x00' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) + s = b'\x40' + 0x40 * b'a' + b'\x00' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_half_pointer(self): - s = '\xc0' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + s = b'\xc0' + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_io_byte(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) - assert d._read_byte() == 'f' - assert d._read_byte() == 'o' - assert d._read_byte() == 'o' + assert d._read_byte() == ord('f') + assert d._read_byte() == ord('o') + assert d._read_byte() == ord('o') def test_io_bytes(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) - assert d._read_bytes(3) == 'foo' + assert d._read_bytes(3) == b'foo' def test_error_io_byte(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) for i in range(3): d._read_byte() @@ -154,13 +161,13 @@ def test_error_io_byte(self): def test_error_io_bytes(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) assert_raises(netlogon.Error, d._read_bytes, 4) def test_error_io_bounds(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) d._set_offset(4) assert_raises(netlogon.Error, d._read_byte) @@ -168,46 +175,42 @@ def test_error_io_bounds(self): def test_error_negative_offset(self): d = netlogon.Decoder() - s = 'foo' + s = b'foo' d.start(s) assert_raises(netlogon.Error, d._set_offset, -1) def test_error_io_type(self): d = netlogon.Decoder() assert_raises(netlogon.Error, d.start, 1) - assert_raises(netlogon.Error, d.start, 1L) + assert_raises(netlogon.Error, d.start, 1) assert_raises(netlogon.Error, d.start, ()) assert_raises(netlogon.Error, d.start, []) assert_raises(netlogon.Error, d.start, {}) assert_raises(netlogon.Error, d.start, u'test') - def test_real_packet(self): - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'netlogon.bin') - fin = file(fname) - buf = fin.read() - fin.close() + def test_real_packet(self, conf): + buf = conf.read_file('protocol/netlogon.bin') dec = netlogon.Decoder() dec.start(buf) res = dec.parse() - assert res.forest == 'freeadi.org' - assert res.domain == 'freeadi.org' - assert res.client_site == 'Default-First-Site' - assert res.server_site == 'Test-Site' + assert res.forest == b'freeadi.org' + assert res.domain == b'freeadi.org' + assert res.client_site == b'Default-First-Site' + assert res.server_site == b'Test-Site' def test_error_short_input(self): - buf = 'x' * 24 + buf = b'x' * 24 dec = netlogon.Decoder() dec.start(buf) assert_raises(netlogon.Error, dec.parse) -class TestClient(BaseTest): +class TestClient(object): """Test suite for netlogon.Client.""" - def test_simple(self): - self.require(ad_user=True) - domain = self.domain() + def test_simple(self, conf): + conf.require(ad_user=True) + domain = conf.domain() client = netlogon.Client() answer = dns.resolver.query('_ldap._tcp.%s' % domain, 'SRV') addrs = [ (ans.target.to_text(), ans.port) for ans in answer ] @@ -233,9 +236,9 @@ def test_simple(self): assert res.q_domain.lower() == domain.lower() assert res.q_timing >= 0.0 - def test_network_failure(self): - self.require(ad_user=True, local_admin=True, firewall=True) - domain = self.domain() + def test_network_failure(self, conf): + conf.require(ad_user=True, local_admin=True, firewall=True) + domain = conf.domain() client = netlogon.Client() answer = dns.resolver.query('_ldap._tcp.%s' % domain, 'SRV') addrs = [ (ans.target.to_text(), ans.port) for ans in answer ] @@ -243,8 +246,8 @@ def test_network_failure(self): client.query(addr, domain) # Block CLDAP traffic and enable it after 3 seconds. Because # NetlogonClient is retrying, it should be succesfull. - self.remove_network_blocks() - self.block_outgoing_traffic('udp', 389) - t = Timer(3, self.remove_network_blocks); t.start() + conf.remove_network_blocks() + conf.block_outgoing_traffic('udp', 389) + t = Timer(3, conf.remove_network_blocks); t.start() result = client.call() assert len(result) == len(addrs) diff --git a/test.conf.example b/tests/test.conf.example similarity index 100% rename from test.conf.example rename to tests/test.conf.example diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9c7a68b --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +envlist = + {python2.7,python3.6,python3.7} + +[testenv] +skipsdist = true +skip_install = true +passenv = PYAD_READONLY_CONFIG PYAD_TEST_CONFIG +commands = + python setup.py build + python setup.py install + pytest tests +deps = + six + python-ldap>=3.0 + dnspython + ply==3.8 + pytest + pexpect + +[testenv:pep8] +description = Run PEP8 pycodestyle (flake8) against the djxml/ package directory +skipsdist = true +skip_install = true +basepython = python3.7 +deps = pycodestyle +commands = pycodestyle lib/activedirectory + +[testenv:clean] +description = Clean all build and test artifacts +skipsdist = true +skip_install = true +deps = +whitelist_externals = + find + rm +commands = + find {toxinidir} -type f -name "*.pyc" -delete + find {toxinidir} -type d -name "__pycache__" -delete + rm -rf {toxworkdir} {toxinidir}/build {toxinidir}/dist diff --git a/tut/tutorial1.py b/tut/tutorial1.py index 48d50bc..fe09aa8 100644 --- a/tut/tutorial1.py +++ b/tut/tutorial1.py @@ -1,4 +1,5 @@ -from ad import Client, Creds, activate +from __future__ import print_function +from activedirectory import Client, Creds, activate domain = 'freeadi.org' user = 'Administrator' @@ -12,4 +13,4 @@ users = client.search('(objectClass=user)') for dn,attrs in users: name = attrs['sAMAccountName'][0] - print '-> %s' % name + print('-> %s' % name) diff --git a/tut/tutorial2.py b/tut/tutorial2.py index 9c4ae56..af498db 100644 --- a/tut/tutorial2.py +++ b/tut/tutorial2.py @@ -1,4 +1,5 @@ -from ad import Client, Creds, activate +from __future__ import print_function +from activedirectory import Client, Creds, activate domain = 'freeadi.org' @@ -11,4 +12,4 @@ for dn,attrs in users: name = attrs['sAMAccountName'][0] domain = client.domain_name_from_dn(dn) - print '-> %s (%s)' % (name, domain) + print('-> %s (%s)' % (name, domain)) diff --git a/tut/tutorial3.py b/tut/tutorial3.py index 9633c89..b8a36e1 100644 --- a/tut/tutorial3.py +++ b/tut/tutorial3.py @@ -1,4 +1,5 @@ -from ad import Client, Creds, Locator, activate +from __future__ import print_function +from activedirectory import Client, Creds, Locator, activate domain = 'freeadi.org' user = 'Administrator' @@ -15,4 +16,4 @@ users = client.search('(objectClass=user)', server=pdc) for dn,attrs in users: name = attrs['sAMAccountName'][0] - print '-> %s' % name + print('-> %s' % name) diff --git a/tut/tutorial4.py b/tut/tutorial4.py index 6994198..ecb9275 100644 --- a/tut/tutorial4.py +++ b/tut/tutorial4.py @@ -1,11 +1,11 @@ -from ad import Client, Creds, Locator, activate +from __future__ import print_function +from activedirectory import Client, Creds, Locator, activate domain = 'freeadi.org' user = 'Administrator' password = 'Pass123' -levels = \ -{ +levels = { '0': 'windows 2000', '1': 'windows 2003 interim', '2': 'windows 2003' @@ -24,4 +24,4 @@ dn, attrs = result[0] level = attrs['forestFunctionality'][0] level = levels.get(level, 'unknown') -print 'Forest functionality level: %s' % level +print('Forest functionality level: %s' % level) diff --git a/tut/tutorial5.py b/tut/tutorial5.py index 5e0fbd7..1a6a6af 100644 --- a/tut/tutorial5.py +++ b/tut/tutorial5.py @@ -1,6 +1,6 @@ import sys -from ad import Client, Creds, Locator, activate -from ad import AD_USERCTRL_NORMAL_ACCOUNT, AD_USERCTRL_ACCOUNT_DISABLED +from activedirectory import Client, Creds, Locator, activate +from activedirectory import AD_USERCTRL_NORMAL_ACCOUNT, AD_USERCTRL_ACCOUNT_DISABLED domain = 'freeadi.org' user = 'Administrator'