From 2163e2094d081ba97ec05d7c90f3be1db161851b Mon Sep 17 00:00:00 2001 From: Sam Powers Date: Tue, 29 Jan 2013 17:53:29 -0800 Subject: [PATCH 01/23] Implement a compat wrapper for the changes to paged results in python-ldap-2.4 --- lib/ad/core/client.py | 10 ++++++---- lib/ad/util/compat.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/lib/ad/core/client.py b/lib/ad/core/client.py index 15c867a..943d31f 100644 --- a/lib/ad/core/client.py +++ b/lib/ad/core/client.py @@ -360,8 +360,7 @@ def _fixup_attrs(self, 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 +368,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 + + size = rctrls[0].size + cookie = rctrls[0].cookie + if not cookie: break ctrl.controlValue = (self._pagesize, cookie) diff --git a/lib/ad/util/compat.py b/lib/ad/util/compat.py index c5d6d73..eb6a68a 100644 --- a/lib/ad/util/compat.py +++ b/lib/ad/util/compat.py @@ -9,6 +9,8 @@ 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: @@ -19,3 +21,43 @@ 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] From 02bd1f8f0efb72215afcf897339e7cd48402d3db Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 25 Mar 2013 21:43:12 -0400 Subject: [PATCH 02/23] Renamed ad => activedirectory --- doc/preface.xml | 2 +- doc/reference.xml | 12 +++---- gentab.py | 4 +-- lib/activedirectory/__init__.py | 17 +++++++++ lib/{ad => activedirectory}/core/__init__.py | 0 lib/{ad => activedirectory}/core/client.py | 14 ++++---- lib/{ad => activedirectory}/core/constant.py | 0 lib/{ad => activedirectory}/core/creds.py | 14 ++++---- lib/{ad => activedirectory}/core/exception.py | 0 lib/{ad => activedirectory}/core/locate.py | 10 +++--- lib/{ad => activedirectory}/core/object.py | 6 ++-- .../core/test/test_client.py | 15 ++++---- .../core/test/test_creds.py | 8 ++--- .../core/test/test_locate.py | 4 +-- .../protocol/__init__.py | 0 lib/{ad => activedirectory}/protocol/asn1.py | 0 lib/{ad => activedirectory}/protocol/krb5.c | 0 lib/{ad => activedirectory}/protocol/ldap.py | 3 +- .../protocol/ldapfilter.py | 2 +- .../protocol/ldapfilter_tab.py | 0 .../protocol/netlogon.py | 4 +-- .../protocol/test/netlogon.bin | Bin .../protocol/test/searchrequest.bin | Bin .../protocol/test/searchresult.bin | Bin .../protocol/test/test_asn1.py | 2 +- .../protocol/test/test_krb5.py | 4 +-- .../protocol/test/test_ldap.py | 18 +++++----- .../protocol/test/test_ldapfilter.py | 4 +-- .../protocol/test/test_netlogon.py | 8 ++--- lib/{ad => activedirectory}/test/__init__.py | 0 lib/{ad => activedirectory}/test/base.py | 2 +- lib/{ad => activedirectory}/util/__init__.py | 0 lib/{ad => activedirectory}/util/compat.py | 0 lib/{ad => activedirectory}/util/log.py | 0 lib/{ad => activedirectory}/util/misc.py | 0 lib/{ad => activedirectory}/util/parser.py | 0 lib/ad/__init__.py | 15 -------- setup.py | 33 ++++++++++-------- tut/tutorial1.py | 2 +- tut/tutorial2.py | 2 +- tut/tutorial3.py | 2 +- tut/tutorial4.py | 2 +- tut/tutorial5.py | 4 +-- 43 files changed, 110 insertions(+), 103 deletions(-) create mode 100644 lib/activedirectory/__init__.py rename lib/{ad => activedirectory}/core/__init__.py (100%) rename lib/{ad => activedirectory}/core/client.py (98%) rename lib/{ad => activedirectory}/core/constant.py (100%) rename lib/{ad => activedirectory}/core/creds.py (97%) rename lib/{ad => activedirectory}/core/exception.py (100%) rename lib/{ad => activedirectory}/core/locate.py (98%) rename lib/{ad => activedirectory}/core/object.py (89%) rename lib/{ad => activedirectory}/core/test/test_client.py (97%) rename lib/{ad => activedirectory}/core/test/test_creds.py (96%) rename lib/{ad => activedirectory}/core/test/test_locate.py (96%) rename lib/{ad => activedirectory}/protocol/__init__.py (100%) rename lib/{ad => activedirectory}/protocol/asn1.py (100%) rename lib/{ad => activedirectory}/protocol/krb5.c (100%) rename lib/{ad => activedirectory}/protocol/ldap.py (99%) rename lib/{ad => activedirectory}/protocol/ldapfilter.py (98%) rename lib/{ad => activedirectory}/protocol/ldapfilter_tab.py (100%) rename lib/{ad => activedirectory}/protocol/netlogon.py (99%) rename lib/{ad => activedirectory}/protocol/test/netlogon.bin (100%) rename lib/{ad => activedirectory}/protocol/test/searchrequest.bin (100%) rename lib/{ad => activedirectory}/protocol/test/searchresult.bin (100%) rename lib/{ad => activedirectory}/protocol/test/test_asn1.py (99%) rename lib/{ad => activedirectory}/protocol/test/test_krb5.py (95%) rename lib/{ad => activedirectory}/protocol/test/test_ldap.py (72%) rename lib/{ad => activedirectory}/protocol/test/test_ldapfilter.py (97%) rename lib/{ad => activedirectory}/protocol/test/test_netlogon.py (97%) rename lib/{ad => activedirectory}/test/__init__.py (100%) rename lib/{ad => activedirectory}/test/base.py (99%) rename lib/{ad => activedirectory}/util/__init__.py (100%) rename lib/{ad => activedirectory}/util/compat.py (100%) rename lib/{ad => activedirectory}/util/log.py (100%) rename lib/{ad => activedirectory}/util/misc.py (100%) rename lib/{ad => activedirectory}/util/parser.py (100%) delete mode 100644 lib/ad/__init__.py 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/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 98% rename from lib/ad/core/client.py rename to lib/activedirectory/core/client.py index 943d31f..6fa30ea 100644 --- a/lib/ad/core/client.py +++ b/lib/activedirectory/core/client.py @@ -15,13 +15,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): 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 97% rename from lib/ad/core/creds.py rename to lib/activedirectory/core/creds.py index 5c77e47..89faa1b 100644 --- a/lib/ad/core/creds.py +++ b/lib/activedirectory/core/creds.py @@ -12,12 +12,12 @@ 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 +64,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 diff --git a/lib/ad/core/exception.py b/lib/activedirectory/core/exception.py similarity index 100% rename from lib/ad/core/exception.py rename to lib/activedirectory/core/exception.py diff --git a/lib/ad/core/locate.py b/lib/activedirectory/core/locate.py similarity index 98% rename from lib/ad/core/locate.py rename to lib/activedirectory/core/locate.py index 14ba67c..07d9b10 100644 --- a/lib/ad/core/locate.py +++ b/lib/activedirectory/core/locate.py @@ -15,10 +15,10 @@ 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 LDAP_PORT = 389 @@ -58,7 +58,7 @@ def __init__(self, site=None): """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 diff --git a/lib/ad/core/object.py b/lib/activedirectory/core/object.py similarity index 89% rename from lib/ad/core/object.py rename to lib/activedirectory/core/object.py index 38bc275..e68b145 100644 --- a/lib/ad/core/object.py +++ b/lib/activedirectory/core/object.py @@ -23,8 +23,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 +35,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/core/test/test_client.py b/lib/activedirectory/core/test/test_client.py similarity index 97% rename from lib/ad/core/test/test_client.py rename to lib/activedirectory/core/test/test_client.py index 647836b..2290755 100644 --- a/lib/ad/core/test/test_client.py +++ b/lib/activedirectory/core/test/test_client.py @@ -8,13 +8,14 @@ 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 +from activedirectory.test.base import BaseTest +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_ACCOUNT_DISABLED, + AD_USERCTRL_NORMAL_ACCOUNT) +from activedirectory.core.creds import Creds +from activedirectory.core.exception import Error as ADError, LDAPError class TestADClient(BaseTest): diff --git a/lib/ad/core/test/test_creds.py b/lib/activedirectory/core/test/test_creds.py similarity index 96% rename from lib/ad/core/test/test_creds.py rename to lib/activedirectory/core/test/test_creds.py index 02a3efc..3101443 100644 --- a/lib/ad/core/test/test_creds.py +++ b/lib/activedirectory/core/test/test_creds.py @@ -9,13 +9,13 @@ 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.test.base import BaseTest +from activedirectory.core.creds import Creds as ADCreds +from activedirectory.core.object import instance, activate class TestCreds(BaseTest): - """Test suite for ad.core.creds.""" + """Test suite for activedirectory.core.creds.""" def test_acquire_password(self): self.require(ad_user=True) diff --git a/lib/ad/core/test/test_locate.py b/lib/activedirectory/core/test/test_locate.py similarity index 96% rename from lib/ad/core/test/test_locate.py rename to lib/activedirectory/core/test/test_locate.py index bd4d2a7..6b88fbf 100644 --- a/lib/ad/core/test/test_locate.py +++ b/lib/activedirectory/core/test/test_locate.py @@ -9,8 +9,8 @@ import math import signal -from ad.test.base import BaseTest -from ad.core.locate import Locator +from activedirectory.test.base import BaseTest +from activedirectory.core.locate import Locator from threading import Timer 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 100% rename from lib/ad/protocol/asn1.py rename to lib/activedirectory/protocol/asn1.py diff --git a/lib/ad/protocol/krb5.c b/lib/activedirectory/protocol/krb5.c similarity index 100% rename from lib/ad/protocol/krb5.c rename to lib/activedirectory/protocol/krb5.c diff --git a/lib/ad/protocol/ldap.py b/lib/activedirectory/protocol/ldap.py similarity index 99% rename from lib/ad/protocol/ldap.py rename to lib/activedirectory/protocol/ldap.py index d27c658..0c97608 100644 --- a/lib/ad/protocol/ldap.py +++ b/lib/activedirectory/protocol/ldap.py @@ -6,8 +6,7 @@ # 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 . import asn1, ldapfilter SCOPE_BASE = 0 diff --git a/lib/ad/protocol/ldapfilter.py b/lib/activedirectory/protocol/ldapfilter.py similarity index 98% rename from lib/ad/protocol/ldapfilter.py rename to lib/activedirectory/protocol/ldapfilter.py index 5dbf9c2..fa7f4a2 100644 --- a/lib/ad/protocol/ldapfilter.py +++ b/lib/activedirectory/protocol/ldapfilter.py @@ -7,7 +7,7 @@ # "AUTHORS" for a complete overview. 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/ad/protocol/ldapfilter_tab.py b/lib/activedirectory/protocol/ldapfilter_tab.py similarity index 100% rename from lib/ad/protocol/ldapfilter_tab.py rename to lib/activedirectory/protocol/ldapfilter_tab.py diff --git a/lib/ad/protocol/netlogon.py b/lib/activedirectory/protocol/netlogon.py similarity index 99% rename from lib/ad/protocol/netlogon.py rename to lib/activedirectory/protocol/netlogon.py index 6311d41..f1ed373 100644 --- a/lib/ad/protocol/netlogon.py +++ b/lib/activedirectory/protocol/netlogon.py @@ -12,8 +12,8 @@ import select import random -from ad.util import misc -from ad.protocol import asn1, ldap +from ..util import misc +from . import asn1, ldap SERVER_PDC = 0x1 diff --git a/lib/ad/protocol/test/netlogon.bin b/lib/activedirectory/protocol/test/netlogon.bin similarity index 100% rename from lib/ad/protocol/test/netlogon.bin rename to lib/activedirectory/protocol/test/netlogon.bin diff --git a/lib/ad/protocol/test/searchrequest.bin b/lib/activedirectory/protocol/test/searchrequest.bin similarity index 100% rename from lib/ad/protocol/test/searchrequest.bin rename to lib/activedirectory/protocol/test/searchrequest.bin diff --git a/lib/ad/protocol/test/searchresult.bin b/lib/activedirectory/protocol/test/searchresult.bin similarity index 100% rename from lib/ad/protocol/test/searchresult.bin rename to lib/activedirectory/protocol/test/searchresult.bin diff --git a/lib/ad/protocol/test/test_asn1.py b/lib/activedirectory/protocol/test/test_asn1.py similarity index 99% rename from lib/ad/protocol/test/test_asn1.py rename to lib/activedirectory/protocol/test/test_asn1.py index a5a7cfe..9f3cf85 100644 --- a/lib/ad/protocol/test/test_asn1.py +++ b/lib/activedirectory/protocol/test/test_asn1.py @@ -6,7 +6,7 @@ # 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 activedirectory.protocol import asn1 from nose.tools import assert_raises diff --git a/lib/ad/protocol/test/test_krb5.py b/lib/activedirectory/protocol/test/test_krb5.py similarity index 95% rename from lib/ad/protocol/test/test_krb5.py rename to lib/activedirectory/protocol/test/test_krb5.py index c577653..cbb1db6 100644 --- a/lib/ad/protocol/test/test_krb5.py +++ b/lib/activedirectory/protocol/test/test_krb5.py @@ -11,8 +11,8 @@ import pexpect from nose.tools import assert_raises -from ad.protocol import krb5 -from ad.test.base import BaseTest, Error +from activedirectory.protocol import krb5 +from activedirectory.test.base import BaseTest, Error class TestKrb5(BaseTest): diff --git a/lib/ad/protocol/test/test_ldap.py b/lib/activedirectory/protocol/test/test_ldap.py similarity index 72% rename from lib/ad/protocol/test/test_ldap.py rename to lib/activedirectory/protocol/test/test_ldap.py index 7178580..e8f8ec3 100644 --- a/lib/ad/protocol/test/test_ldap.py +++ b/lib/activedirectory/protocol/test/test_ldap.py @@ -7,20 +7,20 @@ # "AUTHORS" for a complete overview. import os.path -from ad.test.base import BaseTest -from ad.protocol import ldap +from activedirectory.test.base import BaseTest +from activedirectory.protocol import ldap class TestLDAP(BaseTest): - """Test suite for ad.util.ldap.""" + """Test suite for activedirectory.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') + fname = os.path.join(self.basedir(), + 'lib/activedirectory/protocol/test', 'searchrequest.bin') fin = file(fname) buf = fin.read() fin.close() @@ -28,8 +28,8 @@ def test_encode_real_search_request(self): def test_decode_real_search_reply(self): client = ldap.Client() - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'searchresult.bin') + fname = os.path.join(self.basedir(), + 'lib/activedirectory/protocol/test', 'searchresult.bin') fin = file(fname) buf = fin.read() fin.close() @@ -40,8 +40,8 @@ def test_decode_real_search_reply(self): msgid, dn, attrs = reply[0] assert msgid == 4 assert dn == '' - fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', - 'netlogon.bin') + fname = os.path.join(self.basedir(), + 'lib/activedirectory/protocol/test', 'netlogon.bin') fin = file(fname) netlogon = fin.read() fin.close() diff --git a/lib/ad/protocol/test/test_ldapfilter.py b/lib/activedirectory/protocol/test/test_ldapfilter.py similarity index 97% rename from lib/ad/protocol/test/test_ldapfilter.py rename to lib/activedirectory/protocol/test/test_ldapfilter.py index ce33ff9..c9708aa 100644 --- a/lib/ad/protocol/test/test_ldapfilter.py +++ b/lib/activedirectory/protocol/test/test_ldapfilter.py @@ -7,11 +7,11 @@ # "AUTHORS" for a complete overview. from nose.tools import assert_raises -from ad.protocol import ldapfilter +from activedirectory.protocol import ldapfilter 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/lib/activedirectory/protocol/test/test_netlogon.py similarity index 97% rename from lib/ad/protocol/test/test_netlogon.py rename to lib/activedirectory/protocol/test/test_netlogon.py index 4e8cef4..f227211 100644 --- a/lib/ad/protocol/test/test_netlogon.py +++ b/lib/activedirectory/protocol/test/test_netlogon.py @@ -12,8 +12,8 @@ from threading import Timer from nose.tools import assert_raises -from ad.test.base import BaseTest -from ad.protocol import netlogon +from activedirectory.test.base import BaseTest +from activedirectory.protocol import netlogon class TestDecoder(BaseTest): @@ -182,8 +182,8 @@ def test_error_io_type(self): 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') + fname = os.path.join(self.basedir(), + 'lib/activedirectory/protocol/test', 'netlogon.bin') fin = file(fname) buf = fin.read() fin.close() diff --git a/lib/ad/test/__init__.py b/lib/activedirectory/test/__init__.py similarity index 100% rename from lib/ad/test/__init__.py rename to lib/activedirectory/test/__init__.py diff --git a/lib/ad/test/base.py b/lib/activedirectory/test/base.py similarity index 99% rename from lib/ad/test/base.py rename to lib/activedirectory/test/base.py index 7c0b91f..b673aee 100644 --- a/lib/ad/test/base.py +++ b/lib/activedirectory/test/base.py @@ -14,7 +14,7 @@ from nose import SkipTest from ConfigParser import ConfigParser -from ad.util.log import enable_logging +from activedirectory.util.log import enable_logging class Error(Exception): diff --git a/lib/ad/util/__init__.py b/lib/activedirectory/util/__init__.py similarity index 100% rename from lib/ad/util/__init__.py rename to lib/activedirectory/util/__init__.py diff --git a/lib/ad/util/compat.py b/lib/activedirectory/util/compat.py similarity index 100% rename from lib/ad/util/compat.py rename to lib/activedirectory/util/compat.py diff --git a/lib/ad/util/log.py b/lib/activedirectory/util/log.py similarity index 100% rename from lib/ad/util/log.py rename to lib/activedirectory/util/log.py diff --git a/lib/ad/util/misc.py b/lib/activedirectory/util/misc.py similarity index 100% rename from lib/ad/util/misc.py rename to lib/activedirectory/util/misc.py diff --git a/lib/ad/util/parser.py b/lib/activedirectory/util/parser.py similarity index 100% rename from lib/ad/util/parser.py rename to lib/activedirectory/util/parser.py 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/setup.py b/setup.py index 9a0a5bf..f2934ca 100644 --- a/setup.py +++ b/setup.py @@ -9,21 +9,26 @@ 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='0.9', + description='An Active Directory client library for Python', + author='Geert Jansen', + author_email='programmers@theatlantic.com', + url='https://github.com/theatlantic/python-active-directory', + license='MIT', + classifiers=[ + 'Development Status :: 4 - Beta', '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' + package_dir={'': 'lib'}, + packages=[ + 'activedirectory', 'activedirectory.core', 'activedirectory.protocol', + 'activedirectory.util'], + install_requires=['python-ldap', 'dnspython', 'ply'], + ext_modules=[ + Extension('activedirectory.protocol.krb5', + ['lib/activedirectory/protocol/krb5.c'], + libraries=['krb5'])], + test_suite='nose.collector' ) diff --git a/tut/tutorial1.py b/tut/tutorial1.py index 48d50bc..d57a9a8 100644 --- a/tut/tutorial1.py +++ b/tut/tutorial1.py @@ -1,4 +1,4 @@ -from ad import Client, Creds, activate +from activedirectory import Client, Creds, activate domain = 'freeadi.org' user = 'Administrator' diff --git a/tut/tutorial2.py b/tut/tutorial2.py index 9c4ae56..3bb9a7d 100644 --- a/tut/tutorial2.py +++ b/tut/tutorial2.py @@ -1,4 +1,4 @@ -from ad import Client, Creds, activate +from activedirectory import Client, Creds, activate domain = 'freeadi.org' diff --git a/tut/tutorial3.py b/tut/tutorial3.py index 9633c89..44ad819 100644 --- a/tut/tutorial3.py +++ b/tut/tutorial3.py @@ -1,4 +1,4 @@ -from ad import Client, Creds, Locator, activate +from activedirectory import Client, Creds, Locator, activate domain = 'freeadi.org' user = 'Administrator' diff --git a/tut/tutorial4.py b/tut/tutorial4.py index 6994198..f9f43f2 100644 --- a/tut/tutorial4.py +++ b/tut/tutorial4.py @@ -1,4 +1,4 @@ -from ad import Client, Creds, Locator, activate +from activedirectory import Client, Creds, Locator, activate domain = 'freeadi.org' user = 'Administrator' 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' From b8ddcfa565b4ed80f02c24d2e7ae1daee1d389d8 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 25 Mar 2013 22:28:38 -0400 Subject: [PATCH 03/23] Don't call initialize_krb5_error_table on Mac OS X initialize_krb5_error_table is unnecessary with OS X's version of com_err. And besides that, it's an undefined symbol --- lib/activedirectory/protocol/krb5.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/activedirectory/protocol/krb5.c b/lib/activedirectory/protocol/krb5.c index 76a259b..bd4f0a2 100644 --- a/lib/activedirectory/protocol/krb5.c +++ b/lib/activedirectory/protocol/krb5.c @@ -414,7 +414,9 @@ initkrb5(void) { PyObject *module, *dict; +#if !defined(__APPLE__) || !defined(__MACH__) initialize_krb5_error_table(); +#endif module = Py_InitModule("krb5", k5_methods); dict = PyModule_GetDict(module); From 6417d01ae4705dfbc5468ee68b96d69f8e33e9fc Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Wed, 19 Jun 2013 22:11:15 -0400 Subject: [PATCH 04/23] Add .gitignore file --- .gitignore | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac6d72f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/build +/doc/html +/ldapfilter_tab.py +*.egg-info +/lib/*.egg-info +/lib/*.egg +/lib/activedirectory/protocol/krb5.so +*.pyc +*.log +*.egg +*.db +*.pid +pip-log.txt +.DS_Store +*swp From 69f8f601090f1497bb2ace25cc4ffda9e73602e2 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Wed, 19 Jun 2013 22:12:51 -0400 Subject: [PATCH 05/23] Adds setup.cfg, changes version to 0.9post-atl-1.0.0 --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e46aab5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[egg_info] +tag_build = post-atl-1.0.0 +tag_date = false From fcb2e6c2d676df09a85ef1ef50deff832a9768b3 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 11 Nov 2013 10:51:56 -0500 Subject: [PATCH 06/23] Allow creds to be passed as kwarg to activedirectory.Client As the class is currently defined, there are potential threading issues that might arise if different credentials are passed to the client in different threads, as it is currently a singleton. --- lib/activedirectory/core/client.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/activedirectory/core/client.py b/lib/activedirectory/core/client.py index 6fa30ea..e66c2ae 100644 --- a/lib/activedirectory/core/client.py +++ b/lib/activedirectory/core/client.py @@ -36,7 +36,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 +45,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 +55,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): From 5964f6dcc449535731c8a1c4d525ef559094c39e Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 11 Nov 2013 10:53:14 -0500 Subject: [PATCH 07/23] Bump version to 0.9post-atl-1.0.1 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e46aab5..5b4e414 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = post-atl-1.0.0 +tag_build = post-atl-1.0.1 tag_date = false From b59df8a71685d26cd0ded4077a7d00626f192506 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 18 May 2015 15:34:19 -0400 Subject: [PATCH 08/23] Update ldapfilter_tab.py, bump to v0.9post-atl-1.0.2 --- .gitignore | 1 + .../protocol/ldapfilter_tab.py | 43 ++++++++++--------- setup.cfg | 2 +- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index ac6d72f..47ba69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +dist/ /build /doc/html /ldapfilter_tab.py diff --git a/lib/activedirectory/protocol/ldapfilter_tab.py b/lib/activedirectory/protocol/ldapfilter_tab.py index de32df7..65b874e 100644 --- a/lib/activedirectory/protocol/ldapfilter_tab.py +++ b/lib/activedirectory/protocol/ldapfilter_tab.py @@ -1,42 +1,43 @@ # ldapfilter_tab.py # This file is automatically generated. Do not edit. +_tabversion = '3.5' _lr_method = 'LALR' -_lr_signature = 'o;C\x91;G\xcc[\x06G;\xa3\xa3R\xb9\x14' +_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 = { } +_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] = { } + 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 = { } +_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] = { } + 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'",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), + ("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/setup.cfg b/setup.cfg index 5b4e414..6078720 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = post-atl-1.0.1 +tag_build = post-atl-1.0.2 tag_date = false From 21474cf8c5439d21767f42f5310b05b4bb9d1411 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 24 Jul 2017 09:09:08 -0400 Subject: [PATCH 09/23] Stop writing ldapfilter_tab.py in cwd --- lib/activedirectory/protocol/ldapfilter_tab.py | 4 ++-- lib/activedirectory/util/parser.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/activedirectory/protocol/ldapfilter_tab.py b/lib/activedirectory/protocol/ldapfilter_tab.py index 65b874e..bbda4b7 100644 --- a/lib/activedirectory/protocol/ldapfilter_tab.py +++ b/lib/activedirectory/protocol/ldapfilter_tab.py @@ -1,12 +1,12 @@ # ldapfilter_tab.py # This file is automatically generated. Do not edit. -_tabversion = '3.5' +_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 = {} diff --git a/lib/activedirectory/util/parser.py b/lib/activedirectory/util/parser.py index 7b13b24..deb71f9 100644 --- a/lib/activedirectory/util/parser.py +++ b/lib/activedirectory/util/parser.py @@ -42,7 +42,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 From 9804c4685257c0a3734a8452127eefa4f1e021d0 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 24 Jul 2017 11:53:20 -0400 Subject: [PATCH 10/23] prefer pyldap over python-ldap as requirement --- setup.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f2934ca..69cf1cf 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,20 @@ # "AUTHORS" for a complete overview. from setuptools import setup, Extension +import pkg_resources + +# Prefer pyldap over python-ldap +ldap_req = "pyldap" +try: + pkg_resources.get_distribution("python-ldap") +except pkg_resources.DistributionNotFound: + pass +else: + try: + pkg_resources.get_distribution("pyldap") + except pkg_resources.DistributionNotFound: + ldap_req = "python-ldap" + setup( name='python-active-directory', @@ -25,7 +39,7 @@ packages=[ 'activedirectory', 'activedirectory.core', 'activedirectory.protocol', 'activedirectory.util'], - install_requires=['python-ldap', 'dnspython', 'ply'], + install_requires=[ldap_req, 'dnspython', 'ply'], ext_modules=[ Extension('activedirectory.protocol.krb5', ['lib/activedirectory/protocol/krb5.c'], From 7051d52d43619532841590c2b9e75ad58b17119d Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 24 Jul 2017 09:09:30 -0400 Subject: [PATCH 11/23] Bump to v0.9+atl.1.1.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6078720..0aa5c1e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = post-atl-1.0.2 +tag_build = +atl.1.1.0 tag_date = false From 4e27f14eb720fd0ffe0e015cdfa5cd01e408e25a Mon Sep 17 00:00:00 2001 From: David Krauth Date: Fri, 15 Feb 2019 16:17:47 -0500 Subject: [PATCH 12/23] Add python 3 compatibility --- .gitignore | 5 +- README | 6 -- README.rst | 4 + lib/activedirectory/core/client.py | 67 +++++++++-------- lib/activedirectory/core/creds.py | 11 +-- lib/activedirectory/core/exception.py | 1 + lib/activedirectory/core/locate.py | 10 ++- lib/activedirectory/core/object.py | 3 +- lib/activedirectory/core/test/test_client.py | 8 +- lib/activedirectory/core/test/test_creds.py | 1 + lib/activedirectory/core/test/test_locate.py | 5 +- lib/activedirectory/protocol/asn1.py | 75 ++++++++++--------- lib/activedirectory/protocol/krb5.c | 75 ++++++++++++++++++- lib/activedirectory/protocol/ldap.py | 7 +- lib/activedirectory/protocol/ldapfilter.py | 1 + .../protocol/ldapfilter_tab.py | 2 + lib/activedirectory/protocol/netlogon.py | 28 +++---- .../protocol/test/test_asn1.py | 9 ++- .../protocol/test/test_krb5.py | 1 + .../protocol/test/test_ldap.py | 23 ++---- .../protocol/test/test_ldapfilter.py | 1 + .../protocol/test/test_netlogon.py | 23 +++--- lib/activedirectory/test/base.py | 46 +++++++----- lib/activedirectory/util/compat.py | 1 + lib/activedirectory/util/log.py | 1 + lib/activedirectory/util/misc.py | 1 + lib/activedirectory/util/parser.py | 1 + setup.cfg | 2 +- setup.py | 52 +++++++------ tox.ini | 40 ++++++++++ 30 files changed, 331 insertions(+), 179 deletions(-) delete mode 100644 README create mode 100644 README.rst create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 47ba69d..34f85e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ dist/ *.egg-info /lib/*.egg-info /lib/*.egg -/lib/activedirectory/protocol/krb5.so +/lib/activedirectory/protocol/*.so *.pyc *.log *.egg @@ -14,3 +14,6 @@ dist/ pip-log.txt .DS_Store *swp +.python-version +.tox +venv/ 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..a1c9094 --- /dev/null +++ b/README.rst @@ -0,0 +1,4 @@ +Python-Active-Directory +======================= + +This is Python-AD, an Active Directory client library for Python on UNIX/Linux systems. diff --git a/lib/activedirectory/core/client.py b/lib/activedirectory/core/client.py index e66c2ae..1a65322 100644 --- a/lib/activedirectory/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 @@ -61,7 +62,7 @@ def _credentials(self): creds = instance(Creds) if creds is None or not creds.principal(): m = 'No current credentials or credentials not activated.' - raise ADError, m + raise ADError(m) return creds def _fixup_scheme(self, scheme): @@ -70,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): @@ -132,7 +133,7 @@ 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] @@ -186,7 +187,7 @@ 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 = [] @@ -248,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) @@ -266,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]+|\\*))?') @@ -284,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) @@ -302,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 @@ -321,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): @@ -329,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): @@ -345,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): @@ -357,9 +358,9 @@ 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): @@ -375,7 +376,7 @@ def _search_with_paged_results(self, conn, filter, base, scope, attrs): if c.controlType == compat.LDAP_CONTROL_PAGED_RESULTS ] if not rctrls: m = 'Server does not honour paged results.' - raise ADError, m + raise ADError(m) size = rctrls[0].size cookie = rctrls[0].cookie @@ -404,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 @@ -422,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): @@ -459,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 @@ -531,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) @@ -549,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/activedirectory/core/creds.py b/lib/activedirectory/core/creds.py index 89faa1b..696f3c9 100644 --- a/lib/activedirectory/core/creds.py +++ b/lib/activedirectory/core/creds.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 os import time import logging @@ -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/activedirectory/core/exception.py b/lib/activedirectory/core/exception.py index 662b10c..06cd484 100644 --- a/lib/activedirectory/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/activedirectory/core/locate.py b/lib/activedirectory/core/locate.py index 07d9b10..29fa49f 100644 --- a/lib/activedirectory/core/locate.py +++ b/lib/activedirectory/core/locate.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 time import random import logging @@ -19,6 +20,7 @@ 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 @@ -67,7 +69,7 @@ def locate(self, domain, role=None): 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): @@ -84,7 +86,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() @@ -148,7 +150,7 @@ def _dns_query(self, query, type): self.m_logger.debug('DNS query %s type %s' % (query, type)) try: answer = dns.resolver.query(query, type) - except dns.exception.DNSException, err: + except dns.exception.DNSException as err: answer = [] self.m_logger.error('DNS query error: %s' % (str(err) or err.__doc__)) else: @@ -189,7 +191,7 @@ def _detect_site(self, domain): 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: diff --git a/lib/activedirectory/core/object.py b/lib/activedirectory/core/object.py index e68b145..f406d24 100644 --- a/lib/activedirectory/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 diff --git a/lib/activedirectory/core/test/test_client.py b/lib/activedirectory/core/test/test_client.py index 2290755..3d6ad89 100644 --- a/lib/activedirectory/core/test/test_client.py +++ b/lib/activedirectory/core/test/test_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 from nose.tools import assert_raises from activedirectory.test.base import BaseTest @@ -16,6 +17,7 @@ AD_USERCTRL_NORMAL_ACCOUNT) from activedirectory.core.creds import Creds from activedirectory.core.exception import Error as ADError, LDAPError +from six.moves import range class TestADClient(BaseTest): @@ -249,7 +251,7 @@ def test_incremental_retrieval_of_multivalued_attributes(self): result = client.search('(sAMAccountName=test-usr)') assert len(result) == 1 dn, attrs = result[0] - assert attrs.has_key('memberOf') + assert 'memberOf' in attrs assert len(attrs['memberOf']) == 2000 self._delete_obj(client, user) for group in groups: @@ -283,8 +285,8 @@ def test_search_rootdse(self): 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') + assert 'supportedControl' in attrs + assert 'supportedSASLMechanisms' in attrs def test_search_server(self): self.require(ad_user=True) diff --git a/lib/activedirectory/core/test/test_creds.py b/lib/activedirectory/core/test/test_creds.py index 3101443..142f621 100644 --- a/lib/activedirectory/core/test/test_creds.py +++ b/lib/activedirectory/core/test/test_creds.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 os import pexpect diff --git a/lib/activedirectory/core/test/test_locate.py b/lib/activedirectory/core/test/test_locate.py index 6b88fbf..b6def1f 100644 --- a/lib/activedirectory/core/test/test_locate.py +++ b/lib/activedirectory/core/test/test_locate.py @@ -6,12 +6,15 @@ # 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 activedirectory.test.base import BaseTest from activedirectory.core.locate import Locator from threading import Timer +from six.moves import range class SRV(object): @@ -73,7 +76,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 diff --git a/lib/activedirectory/protocol/asn1.py b/lib/activedirectory/protocol/asn1.py index 6ccef0f..576add2 100644 --- a/lib/activedirectory/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,9 +56,9 @@ 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.' + raise Error('Tag stack is empty.') value = ''.join(self.m_stack[-1]) del self.m_stack[-1] self._emit_length(len(value)) @@ -64,11 +67,11 @@ 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 elif value is None: nr = Null @@ -84,9 +87,9 @@ 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.' + raise Error('Stack is not empty.') output = ''.join(self.m_stack[0]) return output @@ -113,7 +116,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(chr, values)) for val in values: self._emit(val) @@ -136,7 +139,7 @@ def _emit_length_long(self, length): values.append(length & 0xff) length >>= 8 values.reverse() - values = map(chr, values) + values = list(map(chr, values)) # really for correctness as this should not happen anytime soon assert len(values) < 127 head = chr(0x80 | len(values)) @@ -192,7 +195,7 @@ def _encode_integer(self, value): assert i != len(values)-1 values[i] = 0x00 values.reverse() - values = map(chr, values) + values = list(map(chr, values)) return ''.join(values) def _encode_octet_string(self, value): @@ -204,15 +207,15 @@ def _encode_null(self): """Encode a Null value.""" return '' - _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,7 +225,7 @@ def _encode_object_identifier(self, oid): cmp >>= 7 result.append(0x80 | (cmp & 0x7f)) result.reverse() - result = map(chr, result) + result = list(map(chr, result)) return ''.join(result) @@ -237,7 +240,7 @@ def __init__(self): def start(self, data): """Start processing `data'.""" if not isinstance(data, str): - raise Error, 'Expecting string instance.' + raise Error('Expecting string instance.') self.m_stack = [[0, data]] self.m_tag = None @@ -245,7 +248,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 +258,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 +274,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,16 +286,16 @@ 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' + raise Error('ASN1 syntax error') if bytes[0] == '\x00': return False return True @@ -318,10 +321,10 @@ 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 + length = 0 for byte in bytes: length = (length << 8) | byte try: @@ -355,7 +358,7 @@ def _read_byte(self): try: byte = ord(input[index]) except IndexError: - raise Error, 'Premature end of input.' + raise Error('Premature end of input.') self.m_stack[-1][0] += 1 return byte @@ -365,7 +368,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 @@ -382,7 +385,7 @@ def _decode_integer(self, bytes): 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 +397,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 +415,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): @@ -422,13 +425,13 @@ def _decode_object_identifier(self, bytes): for i in range(len(bytes)): byte = ord(bytes[i]) 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) + result = list(map(str, result)) return '.'.join(result) diff --git a/lib/activedirectory/protocol/krb5.c b/lib/activedirectory/protocol/krb5.c index bd4f0a2..08343cc 100644 --- a/lib/activedirectory/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,8 +430,42 @@ 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; @@ -418,8 +473,24 @@ initkrb5(void) 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); + 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/activedirectory/protocol/ldap.py b/lib/activedirectory/protocol/ldap.py index 0c97608..2eec7b3 100644 --- a/lib/activedirectory/protocol/ldap.py +++ b/lib/activedirectory/protocol/ldap.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 from . import asn1, ldapfilter @@ -188,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/activedirectory/protocol/ldapfilter.py b/lib/activedirectory/protocol/ldapfilter.py index fa7f4a2..5841540 100644 --- a/lib/activedirectory/protocol/ldapfilter.py +++ b/lib/activedirectory/protocol/ldapfilter.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 from activedirectory.util.parser import Parser as PLYParser diff --git a/lib/activedirectory/protocol/ldapfilter_tab.py b/lib/activedirectory/protocol/ldapfilter_tab.py index bbda4b7..31b3df6 100644 --- a/lib/activedirectory/protocol/ldapfilter_tab.py +++ b/lib/activedirectory/protocol/ldapfilter_tab.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import +from six.moves import zip # ldapfilter_tab.py # This file is automatically generated. Do not edit. diff --git a/lib/activedirectory/protocol/netlogon.py b/lib/activedirectory/protocol/netlogon.py index f1ed373..9defa50 100644 --- a/lib/activedirectory/protocol/netlogon.py +++ b/lib/activedirectory/protocol/netlogon.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 time import errno import socket @@ -14,6 +15,7 @@ from ..util import misc from . import asn1, ldap +from six.moves import range SERVER_PDC = 0x1 @@ -80,14 +82,14 @@ def _decode_rfc1035(self, _pointer=False): byte = self._read_byte() ptr = ((tag & ~0xc0) << 8) + ord(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) @@ -105,10 +107,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 |= (int(ord(byte)) << i*8) value = self._try_convert_int(value) return value @@ -119,7 +121,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): @@ -129,7 +131,7 @@ def _buffer(self): def _set_buffer(self, buffer): """Set the current buffer.""" if not isinstance(buffer, str): - raise Error, 'Buffer must be plain string.' + raise Error('Buffer must be plain string.') self.m_buffer = buffer def _read_byte(self, offset=None): @@ -140,7 +142,7 @@ 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 update_offset: self.m_offset += 1 @@ -156,7 +158,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 +247,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 +262,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: @@ -320,7 +322,7 @@ def _parse_netlogon_reply(self, reply): return msgid, dn, attrs = messages[0] if not attrs.get('netlogon'): - raise Error, 'No netlogon attribute received.' + raise Error('No netlogon attribute received.') data = attrs['netlogon'][0] decoder = Decoder() decoder.start(data) diff --git a/lib/activedirectory/protocol/test/test_asn1.py b/lib/activedirectory/protocol/test/test_asn1.py index 9f3cf85..852f759 100644 --- a/lib/activedirectory/protocol/test/test_asn1.py +++ b/lib/activedirectory/protocol/test/test_asn1.py @@ -6,6 +6,7 @@ # 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 from activedirectory.protocol import asn1 from nose.tools import assert_raises @@ -30,7 +31,7 @@ def test_integer(self): 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' @@ -44,7 +45,7 @@ def test_negative_integer(self): 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' @@ -266,7 +267,7 @@ def test_long_integer(self): 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' @@ -280,7 +281,7 @@ def test_long_negative_integer(self): 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' diff --git a/lib/activedirectory/protocol/test/test_krb5.py b/lib/activedirectory/protocol/test/test_krb5.py index cbb1db6..96386e5 100644 --- a/lib/activedirectory/protocol/test/test_krb5.py +++ b/lib/activedirectory/protocol/test/test_krb5.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 os import stat import pexpect diff --git a/lib/activedirectory/protocol/test/test_ldap.py b/lib/activedirectory/protocol/test/test_ldap.py index e8f8ec3..e77cd35 100644 --- a/lib/activedirectory/protocol/test/test_ldap.py +++ b/lib/activedirectory/protocol/test/test_ldap.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 os.path from activedirectory.test.base import BaseTest from activedirectory.protocol import ldap @@ -19,20 +20,13 @@ def test_encode_real_search_request(self): 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/activedirectory/protocol/test', 'searchrequest.bin') - fin = file(fname) - buf = fin.read() - fin.close() + + buf = self.read_file('lib/activedirectory/protocol/test/searchrequest.bin') assert req == buf def test_decode_real_search_reply(self): client = ldap.Client() - fname = os.path.join(self.basedir(), - 'lib/activedirectory/protocol/test', 'searchresult.bin') - fin = file(fname) - buf = fin.read() - fin.close() + buf = self.read_file('lib/activedirectory/protocol/test/searchresult.bin') reply = client.parse_message_header(buf) assert reply == (4, 4) reply = client.parse_search_result(buf) @@ -40,9 +34,8 @@ def test_decode_real_search_reply(self): msgid, dn, attrs = reply[0] assert msgid == 4 assert dn == '' - fname = os.path.join(self.basedir(), - 'lib/activedirectory/protocol/test', 'netlogon.bin') - fin = file(fname) - netlogon = fin.read() - fin.close() + + netlogon = self.read_file('lib/activedirectory/protocol/test/netlogon.bin') + print(repr(attrs)) + print(repr({ 'netlogon': [netlogon] })) assert attrs == { 'netlogon': [netlogon] } diff --git a/lib/activedirectory/protocol/test/test_ldapfilter.py b/lib/activedirectory/protocol/test/test_ldapfilter.py index c9708aa..39262be 100644 --- a/lib/activedirectory/protocol/test/test_ldapfilter.py +++ b/lib/activedirectory/protocol/test/test_ldapfilter.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 from nose.tools import assert_raises from activedirectory.protocol import ldapfilter diff --git a/lib/activedirectory/protocol/test/test_netlogon.py b/lib/activedirectory/protocol/test/test_netlogon.py index f227211..112d2c4 100644 --- a/lib/activedirectory/protocol/test/test_netlogon.py +++ b/lib/activedirectory/protocol/test/test_netlogon.py @@ -6,11 +6,15 @@ # 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 + +import six +from six.moves import range + from nose.tools import assert_raises from activedirectory.test.base import BaseTest from activedirectory.protocol import netlogon @@ -39,9 +43,9 @@ def test_uint32_byte_order(self): def test_uint32_long(self): s = '\x00\x00\x00\xff' - assert self.decode_uint32(s, 0) == (0xff000000L, 4) + assert self.decode_uint32(s, 0) == (0xff000000, 4) s = '\xff\xff\xff\xff' - assert self.decode_uint32(s, 0) == (0xffffffffL, 4) + assert self.decode_uint32(s, 0) == (0xffffffff, 4) def test_error_uint32_null_input(self): s = '' @@ -175,18 +179,17 @@ def test_error_negative_offset(self): 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') + if six.PY3: + assert_raises(netlogon.Error, d.start, b'test') + else: + assert_raises(netlogon.Error, d.start, u'test') def test_real_packet(self): - fname = os.path.join(self.basedir(), - 'lib/activedirectory/protocol/test', 'netlogon.bin') - fin = file(fname) - buf = fin.read() - fin.close() + buf = self.read_file('lib/activedirectory/protocol/test/netlogon.bin') dec = netlogon.Decoder() dec.start(buf) res = dec.parse() diff --git a/lib/activedirectory/test/base.py b/lib/activedirectory/test/base.py index b673aee..1c66b38 100644 --- a/lib/activedirectory/test/base.py +++ b/lib/activedirectory/test/base.py @@ -6,14 +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 os.path import sys +import os.path +from io import open import tempfile -import pexpect +import six +from six.moves import range +from six.moves.configparser import ConfigParser +import pexpect from nose import SkipTest -from ConfigParser import ConfigParser + from activedirectory.util.log import enable_logging @@ -29,9 +34,9 @@ def setup_class(cls): config = ConfigParser() fname = os.environ.get('FREEADI_TEST_CONFIG') if fname is None: - raise Error, 'Python-AD test configuration file not specified.' + 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.' + raise Error('Python-AD test configuration file does not exist.') config.read(fname) cls.c_config = config cls.c_basedir = os.path.dirname(fname) @@ -74,34 +79,41 @@ def tempfile(self, contents=None, remove=False): 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.decode('latin_1') if six.PY3 else 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' + raise SkipTest('test disabled by configuration') if not config.get('test', 'domain'): - raise SkipTest, 'ad tests enabled but no domain given' + 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' + raise SkipTest('readonly ad tests enabled but no user/pw given') if local_admin: if not config.getboolean('test', 'intrusive_local_tests'): - raise SkipTest, 'test disabled by configuration' + raise SkipTest('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 SkipTest('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 SkipTest('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' + 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 SkipTest('iptables/conntrack not available') if expensive and not config.getboolean('test', 'expensive_tests'): - raise SkipTest, 'test disabled by configuration' + raise SkipTest('test disabled by configuration') def domain(self): config = self.config() @@ -148,7 +160,7 @@ def execute_as_root(self, command): 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 +175,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 +185,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) diff --git a/lib/activedirectory/util/compat.py b/lib/activedirectory/util/compat.py index eb6a68a..397672c 100644 --- a/lib/activedirectory/util/compat.py +++ b/lib/activedirectory/util/compat.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 ldap import ldap.dn diff --git a/lib/activedirectory/util/log.py b/lib/activedirectory/util/log.py index afc0eec..7f1a205 100644 --- a/lib/activedirectory/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/activedirectory/util/misc.py b/lib/activedirectory/util/misc.py index ad7fbed..f48dd0a 100644 --- a/lib/activedirectory/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/activedirectory/util/parser.py b/lib/activedirectory/util/parser.py index deb71f9..0ac9f3d 100644 --- a/lib/activedirectory/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 diff --git a/setup.cfg b/setup.cfg index 0aa5c1e..c49b970 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = +atl.1.1.0 +tag_build = +atl.2.0.0 tag_date = false diff --git a/setup.py b/setup.py index 69cf1cf..d9d99b0 100644 --- a/setup.py +++ b/setup.py @@ -5,44 +5,48 @@ # # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file # "AUTHORS" for a complete overview. - from setuptools import setup, Extension -import pkg_resources - -# Prefer pyldap over python-ldap -ldap_req = "pyldap" -try: - pkg_resources.get_distribution("python-ldap") -except pkg_resources.DistributionNotFound: - pass -else: - try: - pkg_resources.get_distribution("pyldap") - except pkg_resources.DistributionNotFound: - ldap_req = "python-ldap" setup( name='python-active-directory', - version='0.9', + version='1.0.0', 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 :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python'], + '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', + ], package_dir={'': 'lib'}, packages=[ - 'activedirectory', 'activedirectory.core', 'activedirectory.protocol', - 'activedirectory.util'], - install_requires=[ldap_req, 'dnspython', 'ply'], - ext_modules=[ - Extension('activedirectory.protocol.krb5', - ['lib/activedirectory/protocol/krb5.c'], - libraries=['krb5'])], + 'activedirectory', + 'activedirectory.core', + 'activedirectory.protocol', + 'activedirectory.util' + ], + tests_require=['nose', 'pexpect'], + install_requires=['python-ldap>=3.0', 'dnspython', 'ply', '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/tox.ini b/tox.ini new file mode 100644 index 0000000..6388446 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +envlist = + {py27,py36,py37} + +[testenv] +setenv = FREEADI_TEST_CONFIG = {toxinidir}/test.conf.example +skipsdist = true +skip_install = true +commands = + python setup.py build + python setup.py install + python setup.py test +deps = + six + python-ldap>=3.0 + dnspython + ply + nose + pexpect + +[testenv:pep8] +description = Run PEP8 pycodestyle (flake8) against the djxml/ package directory +skipsdist = true +skip_install = true +basepython = python3.6 +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 From 0f51db0970813b8c21b14af9a3a3d67b5dd852d2 Mon Sep 17 00:00:00 2001 From: David Krauth Date: Thu, 21 Feb 2019 13:07:49 -0500 Subject: [PATCH 13/23] fixes for enabled tests --- lib/activedirectory/core/test/test_client.py | 2 ++ setup.cfg | 3 +++ setup.py | 2 +- tox.ini | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/activedirectory/core/test/test_client.py b/lib/activedirectory/core/test/test_client.py index 3d6ad89..7e5c08b 100644 --- a/lib/activedirectory/core/test/test_client.py +++ b/lib/activedirectory/core/test/test_client.py @@ -8,6 +8,7 @@ from __future__ import absolute_import from nose.tools import assert_raises +from nose import SkipTest from activedirectory.test.base import BaseTest from activedirectory.core.object import activate @@ -24,6 +25,7 @@ class TestADClient(BaseTest): """Test suite for ADClient""" def test_search(self): + raise SkipTest('test disabled by configuration') self.require(ad_user=True) domain = self.domain() creds = Creds(domain) diff --git a/setup.cfg b/setup.cfg index c49b970..4f71cf9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [egg_info] tag_build = +atl.2.0.0 tag_date = false + +[nosetests] +# nocapture = true diff --git a/setup.py b/setup.py index d9d99b0..91b5c2f 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ 'activedirectory.util' ], tests_require=['nose', 'pexpect'], - install_requires=['python-ldap>=3.0', 'dnspython', 'ply', 'six'], + install_requires=['python-ldap>=3.0', 'dnspython', 'ply==3.8', 'six'], ext_modules=[Extension( 'activedirectory.protocol.krb5', ['lib/activedirectory/protocol/krb5.c'], diff --git a/tox.ini b/tox.ini index 6388446..5887b1a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py27,py36,py37} [testenv] -setenv = FREEADI_TEST_CONFIG = {toxinidir}/test.conf.example +setenv = FREEADI_TEST_CONFIG = {env:FREEADI_TEST_CONFIG:{toxinidir}/test.conf.example} skipsdist = true skip_install = true commands = From 518d93b5751d73204e4e27825f28377fb23f6621 Mon Sep 17 00:00:00 2001 From: David Krauth <$USER@theatlantic.com> Date: Fri, 22 Feb 2019 18:28:50 -0500 Subject: [PATCH 14/23] Refactored tests out of package to use pytest --- .gitignore | 2 + README.rst | 52 +++ lib/activedirectory/core/locate.py | 7 +- lib/activedirectory/protocol/netlogon.py | 3 +- .../protocol/test/test_krb5.py | 57 ---- .../protocol/test/test_ldap.py | 41 --- lib/activedirectory/test/__init__.py | 1 - tests/__init__.py | 0 {lib/activedirectory/test => tests}/base.py | 158 ++++----- tests/conftest.py | 7 + tests/core/__init__.py | 0 .../core/test => tests/core}/test_client.py | 300 +++++++----------- .../core/test => tests/core}/test_creds.py | 77 +++-- .../core/test => tests/core}/test_locate.py | 31 +- tests/core/utils.py | 72 +++++ tests/protocol/__init__.py | 0 .../test => tests/protocol}/netlogon.bin | Bin .../test => tests/protocol}/searchrequest.bin | Bin .../test => tests/protocol}/searchresult.bin | Bin .../test => tests/protocol}/test_asn1.py | 3 +- tests/protocol/test_krb5.py | 54 ++++ tests/protocol/test_ldap.py | 39 +++ .../protocol}/test_ldapfilter.py | 3 +- .../test => tests/protocol}/test_netlogon.py | 115 +++---- test.conf.example => tests/test.conf.example | 0 tox.ini | 8 +- 26 files changed, 550 insertions(+), 480 deletions(-) delete mode 100644 lib/activedirectory/protocol/test/test_krb5.py delete mode 100644 lib/activedirectory/protocol/test/test_ldap.py delete mode 100644 lib/activedirectory/test/__init__.py create mode 100644 tests/__init__.py rename {lib/activedirectory/test => tests}/base.py (62%) create mode 100644 tests/conftest.py create mode 100644 tests/core/__init__.py rename {lib/activedirectory/core/test => tests/core}/test_client.py (50%) rename {lib/activedirectory/core/test => tests/core}/test_creds.py (74%) rename {lib/activedirectory/core/test => tests/core}/test_locate.py (79%) create mode 100644 tests/core/utils.py create mode 100644 tests/protocol/__init__.py rename {lib/activedirectory/protocol/test => tests/protocol}/netlogon.bin (100%) rename {lib/activedirectory/protocol/test => tests/protocol}/searchrequest.bin (100%) rename {lib/activedirectory/protocol/test => tests/protocol}/searchresult.bin (100%) rename {lib/activedirectory/protocol/test => tests/protocol}/test_asn1.py (99%) create mode 100644 tests/protocol/test_krb5.py create mode 100644 tests/protocol/test_ldap.py rename {lib/activedirectory/protocol/test => tests/protocol}/test_ldapfilter.py (99%) rename {lib/activedirectory/protocol/test => tests/protocol}/test_netlogon.py (68%) rename test.conf.example => tests/test.conf.example (100%) diff --git a/.gitignore b/.gitignore index 34f85e6..917bf8b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist/ /build /doc/html /ldapfilter_tab.py +.eggs/ *.egg-info /lib/*.egg-info /lib/*.egg @@ -17,3 +18,4 @@ pip-log.txt .python-version .tox venv/ +test.conf.atl diff --git a/README.rst b/README.rst index a1c9094..3333b05 100644 --- a/README.rst +++ b/README.rst @@ -2,3 +2,55 @@ 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/lib/activedirectory/core/locate.py b/lib/activedirectory/core/locate.py index 29fa49f..382960f 100644 --- a/lib/activedirectory/core/locate.py +++ b/lib/activedirectory/core/locate.py @@ -172,7 +172,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') @@ -304,9 +304,8 @@ def _select_domain_controllers(self, replies, role, maxservers, addresses): 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: a.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/activedirectory/protocol/netlogon.py b/lib/activedirectory/protocol/netlogon.py index 9defa50..069a8aa 100644 --- a/lib/activedirectory/protocol/netlogon.py +++ b/lib/activedirectory/protocol/netlogon.py @@ -15,6 +15,7 @@ from ..util import misc from . import asn1, ldap +import six from six.moves import range @@ -306,7 +307,7 @@ def _create_netlogon_query(self, domain, msgid): attrs = ('NetLogon',) query = client.create_search_request('', filter, attrs=attrs, scope=ldap.SCOPE_BASE, msgid=msgid) - return query + return six.ensure_binary(query) def _parse_message_header(self, reply): """Parse an LDAP header and return the messageid and opcode.""" diff --git a/lib/activedirectory/protocol/test/test_krb5.py b/lib/activedirectory/protocol/test/test_krb5.py deleted file mode 100644 index 96386e5..0000000 --- a/lib/activedirectory/protocol/test/test_krb5.py +++ /dev/null @@ -1,57 +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 __future__ import absolute_import -import os -import stat -import pexpect - -from nose.tools import assert_raises -from activedirectory.protocol import krb5 -from activedirectory.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/activedirectory/protocol/test/test_ldap.py b/lib/activedirectory/protocol/test/test_ldap.py deleted file mode 100644 index e77cd35..0000000 --- a/lib/activedirectory/protocol/test/test_ldap.py +++ /dev/null @@ -1,41 +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 __future__ import absolute_import -import os.path -from activedirectory.test.base import BaseTest -from activedirectory.protocol import ldap - - -class TestLDAP(BaseTest): - """Test suite for activedirectory.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) - - buf = self.read_file('lib/activedirectory/protocol/test/searchrequest.bin') - assert req == buf - - def test_decode_real_search_reply(self): - client = ldap.Client() - buf = self.read_file('lib/activedirectory/protocol/test/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 == '' - - netlogon = self.read_file('lib/activedirectory/protocol/test/netlogon.bin') - print(repr(attrs)) - print(repr({ 'netlogon': [netlogon] })) - assert attrs == { 'netlogon': [netlogon] } diff --git a/lib/activedirectory/test/__init__.py b/lib/activedirectory/test/__init__.py deleted file mode 100644 index 792d600..0000000 --- a/lib/activedirectory/test/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/activedirectory/test/base.py b/tests/base.py similarity index 62% rename from lib/activedirectory/test/base.py rename to tests/base.py index 1c66b38..bea6fd1 100644 --- a/lib/activedirectory/test/base.py +++ b/tests/base.py @@ -17,70 +17,86 @@ from six.moves import range from six.moves.configparser import ConfigParser import pexpect -from nose import SkipTest +import pytest from activedirectory.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 = [] + 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) + fname = os.path.join(self.basedir, fname) with open(fname, 'rb') as fin: buf = fin.read() @@ -90,70 +106,58 @@ 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) @@ -202,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) @@ -230,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/lib/activedirectory/core/test/test_client.py b/tests/core/test_client.py similarity index 50% rename from lib/activedirectory/core/test/test_client.py rename to tests/core/test_client.py index 7e5c08b..4dcb704 100644 --- a/lib/activedirectory/core/test/test_client.py +++ b/tests/core/test_client.py @@ -7,157 +7,120 @@ # "AUTHORS" for a complete overview. from __future__ import absolute_import -from nose.tools import assert_raises -from nose import SkipTest +import pytest -from activedirectory.test.base import BaseTest 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_ACCOUNT_DISABLED, - AD_USERCTRL_NORMAL_ACCOUNT) +from activedirectory.core.constant import AD_USERCTRL_NORMAL_ACCOUNT from activedirectory.core.creds import Creds -from activedirectory.core.exception import Error as ADError, LDAPError +from activedirectory.core.exception import Error as ADError from six.moves import range +from ..base import assert_raises +from . import utils -class TestADClient(BaseTest): + +class TestADClient(object): """Test suite for ADClient""" - def test_search(self): - raise SkipTest('test disabled by configuration') - self.require(ad_user=True) - domain = self.domain() + def test_search(self, conf): + pytest.skip('test disabled: hanging') + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + 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 _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() + def test_add(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - user = self._create_user(client, 'test-usr') - self._delete_obj(client, user) + user = utils.create_user(client, 'test-usr') + delete_obj(client, user) - def test_delete(self): - self.require(ad_admin=True) - domain = self.domain() + def test_delete(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - dn = self._create_user(client, 'test-usr') + dn = utils.create_user(client, 'test-usr') client.delete(dn) - def test_modify(self): - self.require(ad_admin=True) - domain = self.domain() + def test_modify(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - user = self._create_user(client, 'test-usr') + user = utils.create_user(client, 'test-usr') mods = [] mods.append(('replace', 'sAMAccountName', ['test-usr-2'])) client.modify(user, mods) - self._delete_obj(client, user) + delete_obj(client, user) - def test_modrdn(self): - self.require(ad_admin=True) - domain = self.domain() + def test_modrdn(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + 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 = self._create_user(client, 'test-usr') + 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): - self.require(ad_admin=True) - domain = self.domain() + def test_rename(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + 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 = self._create_user(client, 'test-usr') + 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 = self._create_ou(client, 'test-ou') + 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): - self.require(ad_user=True) - domain = self.domain() + def test_forest(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + 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): - self.require(ad_user=True) - domain = self.domain() + def test_domains(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) activate(creds) client = Client(domain) domains = client.domains() @@ -165,21 +128,21 @@ def test_domains(self): assert domain assert domain.isupper() - def test_naming_contexts(self): - self.require(ad_user=True) - domain = self.domain() + def test_naming_contexts(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + 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): - self.require(ad_user=True) - domain = self.domain() + def test_search_all_domains(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) activate(creds) client = Client(domain) domains = client.domains() @@ -188,98 +151,71 @@ def test_search_all_domains(self): 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() + def test_search_schema(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + 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): - self.require(ad_user=True) - domain = self.domain() + def test_search_configuration(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + 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 _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() + def test_incremental_retrieval_of_multivalued_attributes(self, conf): + conf.require(ad_admin=True, expensive=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - user = self._create_user(client, 'test-usr') + user = utils.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) + 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 - self._delete_obj(client, user) + delete_obj(client, user) for group in groups: - self._delete_group(client, group) + utils.delete_group(client, group) - def test_paged_results(self): - self.require(ad_admin=True, expensive=True) - domain = self.domain() + def test_paged_results(self, conf): + conf.require(ad_admin=True, expensive=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) users = [] for i in range(2000): - user = self._create_user(client, 'test-usr-%04d' % i) + 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: - self._delete_obj(client, user) + delete_obj(client, user) - def test_search_rootdse(self): - self.require(ad_user=True) - domain = self.domain() + def test_search_rootdse(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) activate(creds) locator = Locator() server = locator.locate(domain) @@ -290,11 +226,11 @@ def test_search_rootdse(self): assert 'supportedControl' in attrs assert 'supportedSASLMechanisms' in attrs - def test_search_server(self): - self.require(ad_user=True) - domain = self.domain() + def test_search_server(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) activate(creds) locator = Locator() server = locator.locate(domain) @@ -302,11 +238,11 @@ def test_search_server(self): result = client.search('(objectClass=user)', server=server) assert len(result) > 1 - def test_search_gc(self): - self.require(ad_user=True) - domain = self.domain() + def test_search_gc(self, conf): + conf.require(ad_user=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_user_account(), self.ad_user_password()) + creds.acquire(conf.ad_user_account(), conf.ad_user_password()) activate(creds) client = Client(domain) result = client.search('(objectClass=user)', scheme='gc') @@ -316,14 +252,14 @@ def test_search_gc(self): # 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() + def test_set_password(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - user = self._create_user(client, 'test-usr-1') + user = utils.create_user(client, 'test-usr-1') principal = 'test-usr-1@%s' % domain client.set_password(principal, 'Pass123') mods = [] @@ -333,18 +269,18 @@ def test_set_password(self): creds = Creds(domain) creds.acquire('test-usr-1', 'Pass123') assert_raises(ADError, creds.acquire, 'test-usr-1', 'Pass321') - self._delete_obj(client, user) + delete_obj(client, user) - def test_set_password_target_pdc(self): - self.require(ad_admin=True) - domain = self.domain() + def test_set_password_target_pdc(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.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) + user = utils.create_user(client, 'test-usr-2', server=pdc) principal = 'test-usr-2@%s' % domain client.set_password(principal, 'Pass123', server=pdc) mods = [] @@ -353,18 +289,17 @@ def test_set_password_target_pdc(self): 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) + assert_raises(ADError, creds.acquire, 'test-usr-2','Pass321', server=pdc) + delete_obj(client, user, server=pdc) - def test_change_password(self): - self.require(ad_admin=True) - domain = self.domain() + def test_change_password(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.ad_admin_password()) activate(creds) client = Client(domain) - user = self._create_user(client, 'test-usr-3') + user = utils.create_user(client, 'test-usr-3') principal = 'test-usr-3@%s' % domain client.set_password(principal, 'Pass123') mods = [] @@ -376,18 +311,18 @@ def test_change_password(self): creds = Creds(domain) creds.acquire('test-usr-3', 'Pass456') assert_raises(ADError, creds.acquire, 'test-usr-3', 'Pass321') - self._delete_obj(client, user) + delete_obj(client, user) - def test_change_password_target_pdc(self): - self.require(ad_admin=True) - domain = self.domain() + def test_change_password_target_pdc(self, conf): + conf.require(ad_admin=True) + domain = conf.domain() creds = Creds(domain) - creds.acquire(self.ad_admin_account(), self.ad_admin_password()) + creds.acquire(conf.ad_admin_account(), conf.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) + user = utils.create_user(client, 'test-usr-4', server=pdc) principal = 'test-usr-4@%s' % domain client.set_password(principal, 'Pass123', server=pdc) mods = [] @@ -398,6 +333,5 @@ def test_change_password_target_pdc(self): 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) + assert_raises(ADError, creds.acquire, 'test-usr-4', 'Pass321', server=pdc) + delete_obj(client, user, server=pdc) diff --git a/lib/activedirectory/core/test/test_creds.py b/tests/core/test_creds.py similarity index 74% rename from lib/activedirectory/core/test/test_creds.py rename to tests/core/test_creds.py index 142f621..c3d78ad 100644 --- a/lib/activedirectory/core/test/test_creds.py +++ b/tests/core/test_creds.py @@ -10,20 +10,19 @@ import os import pexpect -from activedirectory.test.base import BaseTest from activedirectory.core.creds import Creds as ADCreds from activedirectory.core.object import instance, activate -class TestCreds(BaseTest): +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() @@ -31,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'] @@ -52,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') @@ -63,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() @@ -102,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) @@ -124,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() @@ -139,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/activedirectory/core/test/test_locate.py b/tests/core/test_locate.py similarity index 79% rename from lib/activedirectory/core/test/test_locate.py rename to tests/core/test_locate.py index b6def1f..ec89713 100644 --- a/lib/activedirectory/core/test/test_locate.py +++ b/tests/core/test_locate.py @@ -11,7 +11,6 @@ import math import signal -from activedirectory.test.base import BaseTest from activedirectory.core.locate import Locator from threading import Timer from six.moves import range @@ -27,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 @@ -41,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 @@ -88,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/activedirectory/protocol/test/netlogon.bin b/tests/protocol/netlogon.bin similarity index 100% rename from lib/activedirectory/protocol/test/netlogon.bin rename to tests/protocol/netlogon.bin diff --git a/lib/activedirectory/protocol/test/searchrequest.bin b/tests/protocol/searchrequest.bin similarity index 100% rename from lib/activedirectory/protocol/test/searchrequest.bin rename to tests/protocol/searchrequest.bin diff --git a/lib/activedirectory/protocol/test/searchresult.bin b/tests/protocol/searchresult.bin similarity index 100% rename from lib/activedirectory/protocol/test/searchresult.bin rename to tests/protocol/searchresult.bin diff --git a/lib/activedirectory/protocol/test/test_asn1.py b/tests/protocol/test_asn1.py similarity index 99% rename from lib/activedirectory/protocol/test/test_asn1.py rename to tests/protocol/test_asn1.py index 852f759..90ea63e 100644 --- a/lib/activedirectory/protocol/test/test_asn1.py +++ b/tests/protocol/test_asn1.py @@ -8,7 +8,8 @@ from __future__ import absolute_import from activedirectory.protocol import asn1 -from nose.tools import assert_raises + +from ..base import assert_raises class TestEncoder(object): 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..6b1459b --- /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 == '' + + netlogon = conf.read_file('protocol/netlogon.bin') + print(repr(attrs)) + print(repr({ 'netlogon': [netlogon] })) + assert attrs == { 'netlogon': [netlogon] } diff --git a/lib/activedirectory/protocol/test/test_ldapfilter.py b/tests/protocol/test_ldapfilter.py similarity index 99% rename from lib/activedirectory/protocol/test/test_ldapfilter.py rename to tests/protocol/test_ldapfilter.py index 39262be..6cb2e25 100644 --- a/lib/activedirectory/protocol/test/test_ldapfilter.py +++ b/tests/protocol/test_ldapfilter.py @@ -7,9 +7,10 @@ # "AUTHORS" for a complete overview. from __future__ import absolute_import -from nose.tools import assert_raises from activedirectory.protocol import ldapfilter +from ..base import assert_raises + class TestLDAPFilterParser(object): """Test suite for activedirectory.protocol.ldapfilter.""" diff --git a/lib/activedirectory/protocol/test/test_netlogon.py b/tests/protocol/test_netlogon.py similarity index 68% rename from lib/activedirectory/protocol/test/test_netlogon.py rename to tests/protocol/test_netlogon.py index 112d2c4..6bd65e6 100644 --- a/lib/activedirectory/protocol/test/test_netlogon.py +++ b/tests/protocol/test_netlogon.py @@ -15,124 +15,127 @@ import six from six.moves import range -from nose.tools import assert_raises -from activedirectory.test.base import BaseTest +import pytest from activedirectory.protocol import netlogon +from ..base import assert_raises -class TestDecoder(BaseTest): - """Test suite for netlogon.Decoder.""" - def decode_uint32(self, buffer, offset): - d = netlogon.Decoder() - d.start(buffer) - d._set_offset(offset) - return d._decode_uint32(), d._offset() +def decode_uint32(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) + 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) + assert decode_uint32(s, 0) == (0x100, 4) s = '\x00\x00\x01\x00' - assert self.decode_uint32(s, 0) == (0x10000, 4) + assert decode_uint32(s, 0) == (0x10000, 4) s = '\x00\x00\x00\x01' - assert self.decode_uint32(s, 0) == (0x1000000, 4) + assert decode_uint32(s, 0) == (0x1000000, 4) def test_uint32_long(self): s = '\x00\x00\x00\xff' - assert self.decode_uint32(s, 0) == (0xff000000, 4) + assert decode_uint32(s, 0) == (0xff000000, 4) s = '\xff\xff\xff\xff' - assert self.decode_uint32(s, 0) == (0xffffffff, 4) + assert decode_uint32(s, 0) == (0xffffffff, 4) def test_error_uint32_null_input(self): s = '' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) + 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) + assert_raises(netlogon.Error, decode_uint32, s, 0) s = '\x00\x00' - assert_raises(netlogon.Error, self.decode_uint32, s, 0) + assert_raises(netlogon.Error, 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() + assert_raises(netlogon.Error, decode_uint32, s, 0) def test_rfc1035_simple(self): s = '\x03foo\x00' - assert self.decode_rfc1035(s, 0) == ('foo', 5) + assert decode_rfc1035(s, 0) == ('foo', 5) def test_rfc1035_multi_component(self): s = '\x03foo\x03bar\x00' - assert self.decode_rfc1035(s, 0) == ('foo.bar', 9) + assert decode_rfc1035(s, 0) == ('foo.bar', 9) def test_rfc1035_pointer(self): s = '\x03foo\x00\xc0\x00' - assert self.decode_rfc1035(s, 5) == ('foo', 7) + assert decode_rfc1035(s, 5) == ('foo', 7) def test_rfc1035_forward_pointer(self): s = '\xc0\x02\x03foo\x00' - assert self.decode_rfc1035(s, 0) == ('foo', 2) + assert decode_rfc1035(s, 0) == ('foo', 2) def test_rfc1035_pointer_component(self): s = '\x03foo\x00\x03bar\xc0\x00' - assert self.decode_rfc1035(s, 5) == ('bar.foo', 11) + assert decode_rfc1035(s, 5) == ('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) + assert decode_rfc1035(s, 9) == ('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) + assert decode_rfc1035(s, 11) == ('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) + assert decode_rfc1035(s, 0) == ('foo', 5) + assert decode_rfc1035(s, 5) == ('bar', 10) def test_rfc1035_null(self): s = '\x00' - assert self.decode_rfc1035(s, 0) == ('', 1) + assert decode_rfc1035(s, 0) == ('', 1) def test_error_rfc1035_null_input(self): s = '' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + 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) + 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) + 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) + 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) + assert_raises(netlogon.Error, decode_rfc1035, s, 0) s = '\x03foo\xc0\x06\x03bar\xc0\x0c\x03baz\xc0\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + 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) + assert_raises(netlogon.Error, decode_rfc1035, s, 0) s = '\x40' + 0x40 * 'a' + '\x00' - assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) + 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) + assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_io_byte(self): d = netlogon.Decoder() @@ -188,8 +191,8 @@ def test_error_io_type(self): else: assert_raises(netlogon.Error, d.start, u'test') - def test_real_packet(self): - buf = self.read_file('lib/activedirectory/protocol/test/netlogon.bin') + def test_real_packet(self, conf): + buf = conf.read_file('protocol/netlogon.bin') dec = netlogon.Decoder() dec.start(buf) res = dec.parse() @@ -205,12 +208,12 @@ def test_error_short_input(self): 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 ] @@ -236,9 +239,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 ] @@ -246,8 +249,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 index 5887b1a..55a2ea3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,19 +3,19 @@ envlist = {py27,py36,py37} [testenv] -setenv = FREEADI_TEST_CONFIG = {env:FREEADI_TEST_CONFIG:{toxinidir}/test.conf.example} skipsdist = true skip_install = true +passenv = PYAD_READONLY_CONFIG PYAD_TEST_CONFIG commands = python setup.py build python setup.py install - python setup.py test + pytest tests deps = six python-ldap>=3.0 dnspython - ply - nose + ply==3.8 + pytest<4.2 pexpect [testenv:pep8] From 722b2c962f542baf3abe4f2de6208febcfcd555a Mon Sep 17 00:00:00 2001 From: David Krauth Date: Tue, 26 Feb 2019 11:35:42 -0500 Subject: [PATCH 15/23] added k5_error assignment --- lib/activedirectory/protocol/krb5.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activedirectory/protocol/krb5.c b/lib/activedirectory/protocol/krb5.c index 08343cc..2435192 100644 --- a/lib/activedirectory/protocol/krb5.c +++ b/lib/activedirectory/protocol/krb5.c @@ -485,7 +485,7 @@ initkrb5(void) dict = PyModule_GetDict(module); struct module_state *st = GETSTATE(module); - st->error = PyErr_NewException("freeadi.protocol.krb5.Error", NULL, NULL); + k5_error = st->error = PyErr_NewException("freeadi.protocol.krb5.Error", NULL, NULL); PyDict_SetItemString(dict, "Error", st->error); From cfd16803f96024407d72ec8df67e900051cbf9dd Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 8 Mar 2019 11:37:32 -0500 Subject: [PATCH 16/23] Bump to v1.0.1 --- setup.cfg | 4 ---- setup.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4f71cf9..655cbb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,2 @@ -[egg_info] -tag_build = +atl.2.0.0 -tag_date = false - [nosetests] # nocapture = true diff --git a/setup.py b/setup.py index 91b5c2f..c7c55b2 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='python-active-directory', - version='1.0.0', + version='1.0.1', description='An Active Directory client library for Python', long_description=open('README.rst').read(), long_description_content_type='text/x-rst', From 23746594ea4b7e039b0d097aee9ca04a55211fd2 Mon Sep 17 00:00:00 2001 From: David Krauth Date: Fri, 25 Oct 2019 18:03:12 -1000 Subject: [PATCH 17/23] Fix print syntax tutorials, env.py --- env.py | 8 ++++---- setup.py | 2 +- tox.ini | 6 +++--- tut/tutorial1.py | 3 ++- tut/tutorial2.py | 3 ++- tut/tutorial3.py | 3 ++- tut/tutorial4.py | 6 +++--- 7 files changed, 17 insertions(+), 14 deletions(-) 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/setup.py b/setup.py index c7c55b2..ccf2a9a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='python-active-directory', - version='1.0.1', + version='1.0.2', description='An Active Directory client library for Python', long_description=open('README.rst').read(), long_description_content_type='text/x-rst', diff --git a/tox.ini b/tox.ini index 55a2ea3..9c7a68b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - {py27,py36,py37} + {python2.7,python3.6,python3.7} [testenv] skipsdist = true @@ -15,14 +15,14 @@ deps = python-ldap>=3.0 dnspython ply==3.8 - pytest<4.2 + pytest pexpect [testenv:pep8] description = Run PEP8 pycodestyle (flake8) against the djxml/ package directory skipsdist = true skip_install = true -basepython = python3.6 +basepython = python3.7 deps = pycodestyle commands = pycodestyle lib/activedirectory diff --git a/tut/tutorial1.py b/tut/tutorial1.py index d57a9a8..fe09aa8 100644 --- a/tut/tutorial1.py +++ b/tut/tutorial1.py @@ -1,3 +1,4 @@ +from __future__ import print_function from activedirectory import Client, Creds, activate domain = 'freeadi.org' @@ -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 3bb9a7d..af498db 100644 --- a/tut/tutorial2.py +++ b/tut/tutorial2.py @@ -1,3 +1,4 @@ +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 44ad819..b8a36e1 100644 --- a/tut/tutorial3.py +++ b/tut/tutorial3.py @@ -1,3 +1,4 @@ +from __future__ import print_function from activedirectory import Client, Creds, Locator, activate domain = 'freeadi.org' @@ -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 f9f43f2..ecb9275 100644 --- a/tut/tutorial4.py +++ b/tut/tutorial4.py @@ -1,11 +1,11 @@ +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) From 169395c19348cf69839a7b5aa21413491ab9c866 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 2 Mar 2020 23:47:59 -0500 Subject: [PATCH 18/23] Fix various and sundry python 3 bytes/str issues activedirectory.protocol.asn1 was operating with str in python 3; this was resulting in a different value being passed to the socket when six.ensure_text was called; all of these protocol modules now use bytes. Also added the ability for a custom dns.resolver.Resolver instance to be set on the activedirectory.core.locate.Locator singleton, so that custom nameservers can be used to resolve the SRV netlogon queries. fixes FUN-2111 --- lib/activedirectory/core/client.py | 8 +- lib/activedirectory/core/locate.py | 16 ++- lib/activedirectory/protocol/asn1.py | 65 +++++---- lib/activedirectory/protocol/netlogon.py | 21 +-- tests/base.py | 2 +- tests/protocol/test_asn1.py | 161 ++++++++++++----------- tests/protocol/test_ldap.py | 4 +- tests/protocol/test_netlogon.py | 111 ++++++++-------- 8 files changed, 201 insertions(+), 187 deletions(-) diff --git a/lib/activedirectory/core/client.py b/lib/activedirectory/core/client.py index 1a65322..8152237 100644 --- a/lib/activedirectory/core/client.py +++ b/lib/activedirectory/core/client.py @@ -137,9 +137,9 @@ def _init_forest(self): 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.""" @@ -193,7 +193,7 @@ def _init_naming_contexts(self): 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 diff --git a/lib/activedirectory/core/locate.py b/lib/activedirectory/core/locate.py index 382960f..9ae31d4 100644 --- a/lib/activedirectory/core/locate.py +++ b/lib/activedirectory/core/locate.py @@ -11,6 +11,7 @@ import random import logging +import six import ldap import dns.resolver import dns.reversename @@ -56,13 +57,14 @@ class Locator(object): _maxservers = 3 _timeout = 300 # cache entries for 5 minutes - def __init__(self, site=None): + def __init__(self, site=None, resolver=None): """Constructor.""" self.m_site = site self.m_site_detected = False 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() def locate(self, domain, role=None): """Locate one domain controller.""" @@ -75,7 +77,7 @@ def locate(self, domain, role=None): 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): @@ -149,7 +151,7 @@ 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) + 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__)) @@ -186,7 +188,7 @@ 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.""" @@ -271,7 +273,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') @@ -300,11 +302,11 @@ 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(key=lambda a: a.index((a.q_hostname, a.q_port))) + 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])) diff --git a/lib/activedirectory/protocol/asn1.py b/lib/activedirectory/protocol/asn1.py index 576add2..e7d15e1 100644 --- a/lib/activedirectory/protocol/asn1.py +++ b/lib/activedirectory/protocol/asn1.py @@ -59,7 +59,7 @@ def leave(self): 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]) + value = b''.join(self.m_stack[-1]) del self.m_stack[-1] self._emit_length(len(value)) self._emit(value) @@ -73,6 +73,8 @@ def write(self, value, nr=None, typ=None, cls=None): nr = Integer 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: @@ -90,7 +92,7 @@ def output(self): 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]) + output = b''.join(self.m_stack[0]) return output def _emit_tag(self, nr, typ, cls): @@ -103,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)) @@ -116,7 +118,7 @@ def _emit_tag_long(self, nr, typ, cls): values.append((nr & 0x7f) | 0x80) nr >>= 7 values.reverse() - values = list(map(chr, values)) + values = list(map(six.int2byte, values)) for val in values: self._emit(val) @@ -130,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).""" @@ -139,17 +141,17 @@ def _emit_length_long(self, length): values.append(length & 0xff) length >>= 8 values.reverse() - values = list(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): @@ -168,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.""" @@ -195,8 +197,8 @@ def _encode_integer(self, value): assert i != len(values)-1 values[i] = 0x00 values.reverse() - values = list(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.""" @@ -205,7 +207,7 @@ def _encode_octet_string(self, value): def _encode_null(self): """Encode a Null value.""" - return '' + return b'' _re_oid = re.compile(r'^[0-9]+(\.[0-9]+)+$') @@ -225,8 +227,8 @@ def _encode_object_identifier(self, oid): cmp >>= 7 result.append(0x80 | (cmp & 0x7f)) result.reverse() - result = list(map(chr, result)) - return ''.join(result) + result = list(map(six.int2byte, result)) + return b''.join(result) class Decoder(object): @@ -239,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 @@ -296,9 +298,10 @@ 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 + byte = bytes[0] + if isinstance(byte, str): + byte = ord(byte) + return (byte != 0) def _read_tag(self): """Read a tag from the input.""" @@ -323,9 +326,11 @@ def _read_length(self): if count == 0x7f: raise Error('ASN1 syntax error') bytes = self._read_bytes(count) - bytes = [ ord(b) for b in bytes ] + 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) @@ -356,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.') self.m_stack[-1][0] += 1 + if isinstance(byte, str): + byte = ord(byte) return byte def _read_bytes(self, count): @@ -380,7 +387,11 @@ 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 @@ -423,7 +434,9 @@ 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') value = (value << 7) | (byte & 0x7f) @@ -433,5 +446,5 @@ def _decode_object_identifier(self, bytes): if len(result) == 0 or result[0] > 1599: raise Error('ASN1 syntax error') result = [result[0] // 40, result[0] % 40] + result[1:] - result = list(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/activedirectory/protocol/netlogon.py b/lib/activedirectory/protocol/netlogon.py index 069a8aa..931956e 100644 --- a/lib/activedirectory/protocol/netlogon.py +++ b/lib/activedirectory/protocol/netlogon.py @@ -76,12 +76,12 @@ 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') _pointer.append(ptr) @@ -94,7 +94,7 @@ def _decode_rfc1035(self, _pointer=False): else: s = self._read_bytes(tag) result.append(s) - result = '.'.join(result) + result = b'.'.join(result) return result def _try_convert_int(self, value): @@ -111,7 +111,7 @@ def _decode_uint32(self): value = 0 for i in range(4): byte = self._read_byte() - value |= (int(ord(byte)) << i*8) + value |= (byte << i*8) value = self._try_convert_int(value) return value @@ -131,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): @@ -145,6 +145,8 @@ def _read_byte(self, offset=None): if offset >= len(self.m_buffer): 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 @@ -305,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 six.ensure_binary(query) def _parse_message_header(self, reply): """Parse an LDAP header and return the messageid and opcode.""" @@ -322,9 +323,9 @@ def _parse_netlogon_reply(self, reply): if not messages: return msgid, dn, attrs = messages[0] - if not attrs.get('netlogon'): + if not attrs.get(b'netlogon'): raise Error('No netlogon attribute received.') - data = attrs['netlogon'][0] + data = attrs[b'netlogon'][0] decoder = Decoder() decoder.start(data) result = decoder.parse() diff --git a/tests/base.py b/tests/base.py index bea6fd1..80994a8 100644 --- a/tests/base.py +++ b/tests/base.py @@ -100,7 +100,7 @@ def read_file(self, fname): with open(fname, 'rb') as fin: buf = fin.read() - return buf.decode('latin_1') if six.PY3 else buf + return buf def require(self, ad_user=False, local_admin=False, ad_admin=False, firewall=False, expensive=False): diff --git a/tests/protocol/test_asn1.py b/tests/protocol/test_asn1.py index 90ea63e..891df1b 100644 --- a/tests/protocol/test_asn1.py +++ b/tests/protocol/test_asn1.py @@ -8,6 +8,7 @@ from __future__ import absolute_import from activedirectory.protocol import asn1 +import six from ..base import assert_raises @@ -20,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(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(-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() @@ -127,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() @@ -137,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() @@ -147,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() @@ -157,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() @@ -166,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() @@ -175,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() @@ -184,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() @@ -193,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() @@ -234,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() @@ -242,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() @@ -264,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 == 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 == -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() @@ -324,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() @@ -365,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() @@ -374,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() @@ -389,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() @@ -398,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() @@ -413,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() @@ -423,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() @@ -433,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() @@ -443,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() @@ -453,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() @@ -470,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() @@ -479,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() @@ -495,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) @@ -505,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_ldap.py b/tests/protocol/test_ldap.py index 6b1459b..c9fdda5 100644 --- a/tests/protocol/test_ldap.py +++ b/tests/protocol/test_ldap.py @@ -31,9 +31,9 @@ def test_decode_real_search_reply(conf): assert len(reply) == 1 msgid, dn, attrs = reply[0] assert msgid == 4 - assert dn == '' + assert dn == b'' netlogon = conf.read_file('protocol/netlogon.bin') print(repr(attrs)) print(repr({ 'netlogon': [netlogon] })) - assert attrs == { 'netlogon': [netlogon] } + assert attrs == { b'netlogon': [netlogon] } diff --git a/tests/protocol/test_netlogon.py b/tests/protocol/test_netlogon.py index 6bd65e6..a95cfcf 100644 --- a/tests/protocol/test_netlogon.py +++ b/tests/protocol/test_netlogon.py @@ -39,121 +39,121 @@ class TestDecoder(object): """Test suite for netlogon.Decoder.""" def test_uint32_simple(self): - s = '\x01\x00\x00\x00' + s = b'\x01\x00\x00\x00' assert decode_uint32(s, 0) == (1, 4) def test_uint32_byte_order(self): - s = '\x00\x01\x00\x00' + s = b'\x00\x01\x00\x00' assert decode_uint32(s, 0) == (0x100, 4) - s = '\x00\x00\x01\x00' + s = b'\x00\x00\x01\x00' assert decode_uint32(s, 0) == (0x10000, 4) - s = '\x00\x00\x00\x01' + s = b'\x00\x00\x00\x01' assert decode_uint32(s, 0) == (0x1000000, 4) def test_uint32_long(self): - s = '\x00\x00\x00\xff' + s = b'\x00\x00\x00\xff' assert decode_uint32(s, 0) == (0xff000000, 4) - s = '\xff\xff\xff\xff' + s = b'\xff\xff\xff\xff' assert decode_uint32(s, 0) == (0xffffffff, 4) def test_error_uint32_null_input(self): - s = '' + s = b'' assert_raises(netlogon.Error, decode_uint32, s, 0) def test_error_uint32_short_input(self): - s = '\x00' + s = b'\x00' assert_raises(netlogon.Error, decode_uint32, s, 0) - s = '\x00\x00' + s = b'\x00\x00' assert_raises(netlogon.Error, decode_uint32, s, 0) - s = '\x00\x00\x00' + s = b'\x00\x00\x00' assert_raises(netlogon.Error, decode_uint32, s, 0) def test_rfc1035_simple(self): - s = '\x03foo\x00' - assert 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 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 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 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 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 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 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 decode_rfc1035(s, 0) == ('foo', 5) - assert 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 decode_rfc1035(s, 0) == ('', 1) + s = b'\x00' + assert decode_rfc1035(s, 0) == (b'', 1) def test_error_rfc1035_null_input(self): - s = '' + s = b'' assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_missing_tag(self): - s = '\x03foo' + s = b'\x03foo' assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_truncated_input(self): - s = '\x04foo' + s = b'\x04foo' assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_pointer_overflow(self): - s = '\xc0\x03' + s = b'\xc0\x03' assert_raises(netlogon.Error, decode_rfc1035, s, 0) def test_error_rfc1035_cyclic_pointer(self): - s = '\xc0\x00' + s = b'\xc0\x00' assert_raises(netlogon.Error, decode_rfc1035, s, 0) - s = '\x03foo\xc0\x06\x03bar\xc0\x0c\x03baz\xc0\x00' + 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' + s = b'\x80' + 0x80 * b'a' + b'\x00' assert_raises(netlogon.Error, decode_rfc1035, s, 0) - s = '\x40' + 0x40 * 'a' + '\x00' + 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' + 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() @@ -161,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) @@ -175,7 +175,7 @@ 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) @@ -186,23 +186,20 @@ def test_error_io_type(self): assert_raises(netlogon.Error, d.start, ()) assert_raises(netlogon.Error, d.start, []) assert_raises(netlogon.Error, d.start, {}) - if six.PY3: - assert_raises(netlogon.Error, d.start, b'test') - else: - assert_raises(netlogon.Error, d.start, u'test') + assert_raises(netlogon.Error, d.start, u'test') 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) From c94b3f73ece5304341e4fc4bfb688c8aa9aa84e8 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 3 Mar 2020 10:21:59 -0500 Subject: [PATCH 19/23] v1.0.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ccf2a9a..e23b611 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='python-active-directory', - version='1.0.2', + version='1.0.3', description='An Active Directory client library for Python', long_description=open('README.rst').read(), long_description_content_type='text/x-rst', From c2c6c5f97228154f8b8558911f6019606277c402 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 3 Mar 2020 11:56:47 -0500 Subject: [PATCH 20/23] Allow setting m_resolve_hostnames on Locator to resolve to IPs --- lib/activedirectory/core/locate.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/activedirectory/core/locate.py b/lib/activedirectory/core/locate.py index 9ae31d4..3a48187 100644 --- a/lib/activedirectory/core/locate.py +++ b/lib/activedirectory/core/locate.py @@ -57,7 +57,7 @@ class Locator(object): _maxservers = 3 _timeout = 300 # cache entries for 5 minutes - def __init__(self, site=None, resolver=None): + def __init__(self, site=None, resolver=None, resolve_hostnames=False): """Constructor.""" self.m_site = site self.m_site_detected = False @@ -65,6 +65,7 @@ def __init__(self, site=None, resolver=None): 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.""" @@ -129,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 From 056c7d28d7fc73220d37f572c1b97ba3f9d9b5b5 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 3 Mar 2020 12:08:04 -0500 Subject: [PATCH 21/23] v1.0.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e23b611..3b71dc3 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='python-active-directory', - version='1.0.3', + version='1.0.4', description='An Active Directory client library for Python', long_description=open('README.rst').read(), long_description_content_type='text/x-rst', From 4f289fa6179b3184767fde524e31a7f0f2f54eff Mon Sep 17 00:00:00 2001 From: Simon Jess Date: Tue, 30 Jun 2020 10:20:12 +0200 Subject: [PATCH 22/23] Adapt requirements of ply==3.8 -> ply>=3.8 --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3b71dc3..a2efd11 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], package_dir={'': 'lib'}, packages=[ @@ -41,7 +42,7 @@ 'activedirectory.util' ], tests_require=['nose', 'pexpect'], - install_requires=['python-ldap>=3.0', 'dnspython', 'ply==3.8', 'six'], + install_requires=['python-ldap>=3.0', 'dnspython', 'ply>=3.8', 'six'], ext_modules=[Extension( 'activedirectory.protocol.krb5', ['lib/activedirectory/protocol/krb5.c'], From a1e2a78a25ea60cbcaf2fc2ec4d5e8743e03ec64 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 6 Jul 2020 16:57:04 -0400 Subject: [PATCH 23/23] v1.0.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a2efd11..1505005 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='python-active-directory', - version='1.0.4', + 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',