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'