├── lib └── ad │ ├── core │ ├── __init__.py │ ├── exception.py │ ├── constant.py │ ├── object.py │ ├── test │ │ ├── test_locate.py │ │ ├── test_creds.py │ │ └── test_client.py │ ├── creds.py │ └── locate.py │ ├── protocol │ ├── __init__.py │ ├── test │ │ ├── netlogon.bin │ │ ├── searchrequest.bin │ │ ├── searchresult.bin │ │ ├── test_ldap.py │ │ ├── test_krb5.py │ │ ├── test_ldapfilter.py │ │ ├── test_netlogon.py │ │ └── test_asn1.py │ ├── ldapfilter_tab.py │ ├── ldapfilter.py │ ├── ldap.py │ ├── netlogon.py │ ├── krb5.c │ └── asn1.py │ ├── test │ ├── __init__.py │ └── base.py │ ├── util │ ├── __init__.py │ ├── compat.py │ ├── misc.py │ ├── log.py │ └── parser.py │ └── __init__.py ├── .hgignore ├── AUTHORS ├── README ├── MANIFEST.in ├── header ├── tut ├── tutorial1.py ├── tutorial2.py ├── tutorial3.py ├── tutorial4.py └── tutorial5.py ├── doc ├── index.xml ├── preface.xml ├── Makefile ├── license.xml └── reference.xml ├── gentab.py ├── LICENSE ├── setup.py ├── env.py └── test.conf.example /lib/ad/core/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /lib/ad/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /lib/ad/test/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /lib/ad/util/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | *.so 4 | build/ 5 | test.conf 6 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/netlogon.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geertj/python-ad/HEAD/lib/ad/protocol/test/netlogon.bin -------------------------------------------------------------------------------- /lib/ad/protocol/test/searchrequest.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geertj/python-ad/HEAD/lib/ad/protocol/test/searchrequest.bin -------------------------------------------------------------------------------- /lib/ad/protocol/test/searchresult.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geertj/python-ad/HEAD/lib/ad/protocol/test/searchresult.bin -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The following people have contributed to Python-AD. Collectively they own 2 | the copyright of this software. 3 | 4 | * Geert Jansen : original implementation 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Python-AD 2 | ========= 3 | 4 | This is Python-AD, an Active Directory client library for Python on UNIX/Linux 5 | systems. For up to date information, see the project home page at 6 | http://code.google.com/p/python-ad/. 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README LICENSE AUTHORS MANIFEST.in 2 | recursive-include lib *.py *.c *.bin 3 | recursive-include tut *.py 4 | recursive-include doc *.xml Makefile 5 | include setup.py env.py gentab.py 6 | include test.conf.example 7 | -------------------------------------------------------------------------------- /header: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | -------------------------------------------------------------------------------- /tut/tutorial1.py: -------------------------------------------------------------------------------- 1 | from ad import Client, Creds, activate 2 | 3 | domain = 'freeadi.org' 4 | user = 'Administrator' 5 | password = 'Pass123' 6 | 7 | creds = Creds(domain) 8 | creds.acquire(user, password) 9 | activate(creds) 10 | 11 | client = Client(domain) 12 | users = client.search('(objectClass=user)') 13 | for dn,attrs in users: 14 | name = attrs['sAMAccountName'][0] 15 | print '-> %s' % name 16 | -------------------------------------------------------------------------------- /tut/tutorial2.py: -------------------------------------------------------------------------------- 1 | from ad import Client, Creds, activate 2 | 3 | domain = 'freeadi.org' 4 | 5 | creds = Creds(domain) 6 | creds.load() 7 | activate(creds) 8 | 9 | client = Client(domain) 10 | users = client.search('(objectClass=user)', scheme='gc') 11 | for dn,attrs in users: 12 | name = attrs['sAMAccountName'][0] 13 | domain = client.domain_name_from_dn(dn) 14 | print '-> %s (%s)' % (name, domain) 15 | -------------------------------------------------------------------------------- /lib/ad/core/exception.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import ldap 10 | 11 | 12 | class Error(Exception): 13 | pass 14 | 15 | 16 | LDAPError = ldap.LDAPError 17 | -------------------------------------------------------------------------------- /tut/tutorial3.py: -------------------------------------------------------------------------------- 1 | from ad import Client, Creds, Locator, activate 2 | 3 | domain = 'freeadi.org' 4 | user = 'Administrator' 5 | password = 'Pass123' 6 | 7 | creds = Creds(domain) 8 | creds.acquire(user, password) 9 | activate(creds) 10 | 11 | locator = Locator() 12 | pdc = locator.locate(domain, role='pdc') 13 | 14 | client = Client(domain) 15 | users = client.search('(objectClass=user)', server=pdc) 16 | for dn,attrs in users: 17 | name = attrs['sAMAccountName'][0] 18 | print '-> %s' % name 19 | -------------------------------------------------------------------------------- /doc/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Python-AD reference manual 6 | Geert Jansen 7 | 20072008 Geert Jansen 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc/preface.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Foreword 6 | 7 | 8 | This is the reference manual for Python-AD. It is the definitive reference 9 | for the Python-AD API. Other information on Python-AD, such as download 10 | instructions, installation instructions, and a tuturial, can be found on the 11 | Python-AD project 12 | page. 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/ad/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | from ad.core.exception import * 10 | from ad.core.constant import * 11 | 12 | from ad.core.client import Client 13 | from ad.core.creds import Creds 14 | from ad.core.locate import Locator 15 | from ad.core.object import activate 16 | -------------------------------------------------------------------------------- /lib/ad/core/constant.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | # Network ports 10 | 11 | LDAP_PORT = 389 12 | GC_PORT = 3268 13 | 14 | # AD constants 15 | 16 | AD_USERCTRL_ACCOUNT_DISABLED = 0x2 17 | AD_USERCTRL_NORMAL_ACCOUNT = 0x200 18 | AD_USERCTRL_WORKSTATION_ACCOUNT = 0x1000 19 | AD_USERCTRL_DONT_EXPIRE_PASSWORD = 0x10000 20 | -------------------------------------------------------------------------------- /gentab.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os 10 | 11 | # This script generates the PLY parser tables. Note: It needs to be run from 12 | # the top-level python-ad directory! 13 | 14 | from ad.protocol.ldapfilter import Parser as LDAPFilterParser 15 | 16 | os.chdir('lib/ad/protocol') 17 | 18 | parser = LDAPFilterParser() 19 | parser._write_parsetab() 20 | -------------------------------------------------------------------------------- /tut/tutorial4.py: -------------------------------------------------------------------------------- 1 | from ad import Client, Creds, Locator, activate 2 | 3 | domain = 'freeadi.org' 4 | user = 'Administrator' 5 | password = 'Pass123' 6 | 7 | levels = \ 8 | { 9 | '0': 'windows 2000', 10 | '1': 'windows 2003 interim', 11 | '2': 'windows 2003' 12 | } 13 | 14 | creds = Creds(domain) 15 | creds.acquire(user, password) 16 | activate(creds) 17 | 18 | locator = Locator() 19 | server = locator.locate(domain) 20 | 21 | client = Client(domain) 22 | result = client.search(base='', scope='base', server=server) 23 | assert len(result) == 1 24 | dn, attrs = result[0] 25 | level = attrs['forestFunctionality'][0] 26 | level = levels.get(level, 'unknown') 27 | print 'Forest functionality level: %s' % level 28 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | XSLTPROC = xsltproc 10 | XSLTFLAGS = --xinclude 11 | STYLESHEET = /usr/share/sgml/docbook/xsl-stylesheets/xhtml/chunk.xsl 12 | MKDIR = mkdir -p 13 | INPUTS = index.xml preface.xml reference.xml license.xml 14 | 15 | all: html/index.html 16 | 17 | html/index.html: $(INPUTS) 18 | $(MKDIR) html 19 | $(XSLTPROC) $(XSLTFLAGS) --output html/index.html $(STYLESHEET) $< 20 | -------------------------------------------------------------------------------- /lib/ad/util/compat.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2009 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | 9 | import ldap 10 | import ldap.dn 11 | 12 | # ldap.str2dn has been removed in python-ldap >= 2.3.6. We now need to use 13 | # the version in ldap.dn. 14 | try: 15 | str2dn = ldap.dn.str2dn 16 | except AttributeError: 17 | str2dn = ldap.str2dn 18 | 19 | def disable_reverse_dns(): 20 | # Possibly add in a Kerberos minimum version check as well... 21 | return hasattr(ldap, 'OPT_X_SASL_NOCANON') 22 | -------------------------------------------------------------------------------- /lib/ad/util/misc.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import socket 10 | 11 | 12 | def hostname(): 13 | """Return the host name. 14 | 15 | The host name is defined as the "short" host name. If the hostname as 16 | returned by gethostname() includes a domain, the part until the first 17 | period ('.') is returned. 18 | """ 19 | hostname = socket.gethostname() 20 | if '.' in hostname: 21 | hostname = hostname.split('.')[0] 22 | return hostname 23 | -------------------------------------------------------------------------------- /lib/ad/util/log.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2009 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | 9 | import sys 10 | import logging 11 | 12 | 13 | def enable_logging(level=None): 14 | """Utility function to enable logging for the "ad" package if no root 15 | logger is configured yet.""" 16 | if level is None: 17 | level = logging.DEBUG 18 | logger = logging.getLogger('ad') 19 | handler = logging.StreamHandler(sys.stdout) 20 | format = '%(levelname)s [%(name)s] %(message)s' 21 | formatter = logging.Formatter(format) 22 | handler.setFormatter(formatter) 23 | logger.addHandler(handler) 24 | logger.setLevel(level) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 the Python-AD authors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | from setuptools import setup, Extension 10 | 11 | setup( 12 | name = 'python-ad', 13 | version = '0.9', 14 | description = 'An AD client library for Python', 15 | author = 'Geert Jansen', 16 | author_email = 'geertj@gmail.com', 17 | url = 'https://github.com/geertj/python-ad', 18 | license = 'MIT', 19 | classifiers = ['Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python'], 23 | package_dir = {'': 'lib'}, 24 | packages = ['ad', 'ad.core', 'ad.protocol', 'ad.util'], 25 | install_requires = [ 'python-ldap', 'dnspython', 'ply' ], 26 | ext_modules = [Extension('ad.protocol.krb5', ['lib/ad/protocol/krb5.c'], 27 | libraries=['krb5'])], 28 | test_suite = 'nose.collector' 29 | ) 30 | -------------------------------------------------------------------------------- /tut/tutorial5.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ad import Client, Creds, Locator, activate 3 | from ad import AD_USERCTRL_NORMAL_ACCOUNT, AD_USERCTRL_ACCOUNT_DISABLED 4 | 5 | domain = 'freeadi.org' 6 | user = 'Administrator' 7 | password = 'Pass123' 8 | 9 | if len(sys.argv) != 3: 10 | sys.stderr.write('Usage: useradd \n') 11 | sys.exit(1) 12 | username = sys.argv[1] 13 | userpass = sys.argv[2] 14 | 15 | creds = Creds(domain) 16 | creds.acquire(user, password) 17 | activate(creds) 18 | 19 | client = Client(domain) 20 | result = client.search('(sAMAccountName=%s)' % username) 21 | if len(result) > 0: 22 | sys.stderr.write('Error: user %s already exists\n' % username) 23 | sys.exit(1) 24 | 25 | dn = 'cn=%s,cn=users,%s' % (username, client.dn_from_domain_name(domain)) 26 | attrs = [] 27 | attrs.append(('cn', [username])) 28 | attrs.append(('sAMAccountName', [username])) 29 | princ = '%s@%s' % (username, domain) 30 | attrs.append(('userPrincipalName', [princ])) 31 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT | AD_USERCTRL_ACCOUNT_DISABLED 32 | attrs.append(('userAccountControl', [str(ctrl)])) 33 | attrs.append(('objectClass', ['user'])) 34 | client.add(dn, attrs) 35 | 36 | client.set_password(princ, userpass) 37 | 38 | mods = [] 39 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT 40 | mods.append(('replace', 'userAccountControl', [str(ctrl)])) 41 | client.modify(dn, mods) 42 | -------------------------------------------------------------------------------- /doc/license.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | License 5 | The Python-AD software is goverend by the following license (the 6 | "MIT" license): 7 | 8 | Copyright (c) 2007-2008 the Python-AD authors. 9 | 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 25 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/ad/core/object.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | 10 | def _singleton(cls, *args, **kwargs): 11 | """Return the single instance of a class, creating it if it does not exist.""" 12 | if not hasattr(cls, 'instance') or cls.instance is None: 13 | obj = apply(cls, args, kwargs) 14 | cls.instance = obj 15 | return cls.instance 16 | 17 | def instance(cls): 18 | """Return the single instance of a class. The instance needs to exist.""" 19 | if not hasattr(cls, 'instance'): 20 | return None 21 | return cls.instance 22 | 23 | def factory(cls): 24 | """Create an instance of a class, creating it using the system specific 25 | rules.""" 26 | from ad.core.locate import Locator 27 | from ad.core.creds import Creds 28 | if issubclass(cls, Locator): 29 | return _singleton(Locator) 30 | elif issubclass(cls, Creds): 31 | domain = detect_domain() 32 | return Creds(domain) 33 | else: 34 | return cls() 35 | 36 | def activate(obj): 37 | """Activate `obj' to be the active instance of its class.""" 38 | from ad.core.creds import Creds 39 | if isinstance(obj, Creds): 40 | obj._activate_config() 41 | obj._activate_ccache() 42 | type(obj).instance = obj 43 | return obj 44 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/test_ldap.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os.path 10 | from ad.test.base import BaseTest 11 | from ad.protocol import ldap 12 | 13 | 14 | class TestLDAP(BaseTest): 15 | """Test suite for ad.util.ldap.""" 16 | 17 | def test_encode_real_search_request(self): 18 | client = ldap.Client() 19 | filter = '(&(DnsDomain=FREEADI.ORG)(Host=magellan)(NtVer=\\06\\00\\00\\00))' 20 | req = client.create_search_request('', filter, ('NetLogon',), 21 | scope=ldap.SCOPE_BASE, msgid=4) 22 | fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', 23 | 'searchrequest.bin') 24 | fin = file(fname) 25 | buf = fin.read() 26 | fin.close() 27 | assert req == buf 28 | 29 | def test_decode_real_search_reply(self): 30 | client = ldap.Client() 31 | fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', 32 | 'searchresult.bin') 33 | fin = file(fname) 34 | buf = fin.read() 35 | fin.close() 36 | reply = client.parse_message_header(buf) 37 | assert reply == (4, 4) 38 | reply = client.parse_search_result(buf) 39 | assert len(reply) == 1 40 | msgid, dn, attrs = reply[0] 41 | assert msgid == 4 42 | assert dn == '' 43 | fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', 44 | 'netlogon.bin') 45 | fin = file(fname) 46 | netlogon = fin.read() 47 | fin.close() 48 | assert attrs == { 'netlogon': [netlogon] } 49 | -------------------------------------------------------------------------------- /env.py: -------------------------------------------------------------------------------- 1 | # vi: ts=4 sw=4 et 2 | # 3 | # env.py: setup environment variabels. 4 | # 5 | # This small utility outputs a bourne shell fragment that sets up the 6 | # PATH and PYTHONPATH environment variables such that Python-AD can be used 7 | # from within its source directory. This is required for the test suite, 8 | # and is helpful for developing. Kudos to the py-lib team for the idea. 9 | # 10 | # This file is part of Python-AD. Python-AD is free software that is made 11 | # available under the MIT license. Consult the file "LICENSE" that is 12 | # distributed together with this file for the exact licensing terms. 13 | # 14 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 15 | # "AUTHORS" for a complete overview. 16 | 17 | import os 18 | import os.path 19 | import sys 20 | 21 | 22 | def prepend_path(name, value): 23 | if sys.platform == 'win32': 24 | sep = ';' 25 | else: 26 | sep = ':' 27 | env_path = os.environ.get(name, '') 28 | parts = [ x for x in env_path.split(sep) if x ] 29 | while value in parts: 30 | del parts[parts.index(value)] 31 | parts.insert(0, value) 32 | return setenv(name, sep.join(parts)) 33 | 34 | def setenv(name, value): 35 | shell = os.environ.get('SHELL', '') 36 | comspec = os.environ.get('COMSPEC', '') 37 | if shell.endswith('csh'): 38 | cmd = 'setenv %s "%s"' % (name, value) 39 | elif shell.endswith('sh'): 40 | cmd = '%s="%s"; export %s' % (name, value, name) 41 | elif comspec.endswith('cmd.exe'): 42 | cmd = '@set %s=%s' % (name, value) 43 | else: 44 | assert False, 'Shell not supported.' 45 | return cmd 46 | 47 | 48 | abspath = os.path.abspath(sys.argv[0]) 49 | topdir, fname = os.path.split(abspath) 50 | 51 | bindir = os.path.join(topdir, 'bin') 52 | print prepend_path('PATH', bindir) 53 | pythondir = os.path.join(topdir, 'lib') 54 | print prepend_path('PYTHONPATH', pythondir) 55 | testconf = os.path.join(topdir, 'test.conf') 56 | print setenv('FREEADI_TEST_CONFIG', testconf) 57 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/test_krb5.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os 10 | import stat 11 | import pexpect 12 | 13 | from nose.tools import assert_raises 14 | from ad.protocol import krb5 15 | from ad.test.base import BaseTest, Error 16 | 17 | 18 | class TestKrb5(BaseTest): 19 | """Test suite for protocol.krb5.""" 20 | 21 | def test_cc_default(self): 22 | self.require(ad_user=True) 23 | domain = self.domain().upper() 24 | principal = '%s@%s' % (self.ad_user_account(), domain) 25 | password = self.ad_user_password() 26 | self.acquire_credentials(principal, password) 27 | ccache = krb5.cc_default() 28 | ccname, princ, creds = self.list_credentials(ccache) 29 | assert princ.lower() == principal.lower() 30 | assert len(creds) > 0 31 | assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) 32 | 33 | def test_cc_copy_creds(self): 34 | self.require(ad_user=True) 35 | domain = self.domain().upper() 36 | principal = '%s@%s' % (self.ad_user_account(), domain) 37 | password = self.ad_user_password() 38 | self.acquire_credentials(principal, password) 39 | ccache = krb5.cc_default() 40 | cctmp = self.tempfile() 41 | assert_raises(Error, self.list_credentials, cctmp) 42 | krb5.cc_copy_creds(ccache, cctmp) 43 | ccname, princ, creds = self.list_credentials(cctmp) 44 | assert princ.lower() == principal.lower() 45 | assert len(creds) > 0 46 | assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) 47 | 48 | def test_cc_get_principal(self): 49 | self.require(ad_user=True) 50 | domain = self.domain().upper() 51 | principal = '%s@%s' % (self.ad_user_account(), domain) 52 | password = self.ad_user_password() 53 | self.acquire_credentials(principal, password) 54 | ccache = krb5.cc_default() 55 | princ = krb5.cc_get_principal(ccache) 56 | assert princ.lower() == principal.lower() 57 | -------------------------------------------------------------------------------- /lib/ad/protocol/ldapfilter_tab.py: -------------------------------------------------------------------------------- 1 | 2 | # ldapfilter_tab.py 3 | # This file is automatically generated. Do not edit. 4 | 5 | _lr_method = 'LALR' 6 | 7 | _lr_signature = 'o;C\x91;G\xcc[\x06G;\xa3\xa3R\xb9\x14' 8 | 9 | _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,]),} 10 | 11 | _lr_action = { } 12 | for _k, _v in _lr_action_items.items(): 13 | for _x,_y in zip(_v[0],_v[1]): 14 | if not _lr_action.has_key(_x): _lr_action[_x] = { } 15 | _lr_action[_x][_k] = _y 16 | del _lr_action_items 17 | 18 | _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,]),} 19 | 20 | _lr_goto = { } 21 | for _k, _v in _lr_goto_items.items(): 22 | for _x,_y in zip(_v[0],_v[1]): 23 | if not _lr_goto.has_key(_x): _lr_goto[_x] = { } 24 | _lr_goto[_x][_k] = _y 25 | del _lr_goto_items 26 | _lr_productions = [ 27 | ("S'",1,None,None,None), 28 | ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',108), 29 | ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',109), 30 | ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',110), 31 | ('filter',3,'p_filter','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',111), 32 | ('and',2,'p_and','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',116), 33 | ('or',2,'p_or','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',120), 34 | ('not',2,'p_not','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',124), 35 | ('filterlist',1,'p_filterlist','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',128), 36 | ('filterlist',2,'p_filterlist','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',129), 37 | ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',137), 38 | ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',138), 39 | ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',139), 40 | ('item',3,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',140), 41 | ('item',2,'p_item','/home/geertj/Projects/python-ad/lib/ad/protocol/ldapfilter.py',141), 42 | ] 43 | -------------------------------------------------------------------------------- /lib/ad/util/parser.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import sys 10 | import os.path 11 | 12 | from ply import lex, yacc 13 | 14 | 15 | class Parser(object): 16 | """Wrapper object for PLY lexer/parser.""" 17 | 18 | exception = ValueError 19 | 20 | def _parsetab_name(cls, fullname=True): 21 | """Return a name for PLY's parsetab file.""" 22 | ptname = sys.modules[cls.__module__].__name__ + '_tab' 23 | if not fullname: 24 | ptname = ptname.split('.')[-1] 25 | return ptname 26 | 27 | _parsetab_name = classmethod(_parsetab_name) 28 | 29 | def _write_parsetab(cls): 30 | """Write parser table (distribution purposes).""" 31 | parser = cls() 32 | tabname = cls._parsetab_name(False) 33 | yacc.yacc(module=parser, debug=0, tabmodule=tabname) 34 | 35 | _write_parsetab = classmethod(_write_parsetab) 36 | 37 | def parse(self, input, fname=None): 38 | lexer = lex.lex(object=self) 39 | if hasattr(input, 'read'): 40 | input = input.read() 41 | lexer.input(input) 42 | self.m_input = input 43 | self.m_fname = fname 44 | parser = yacc.yacc(module=self, debug=0, 45 | tabmodule=self._parsetab_name()) 46 | parsed = parser.parse(lexer=lexer, tracking=True) 47 | return parsed 48 | 49 | def _position(self, o): 50 | if hasattr(o, 'lineno') and hasattr(o, 'lexpos'): 51 | lineno = o.lineno 52 | lexpos = o.lexpos 53 | pos = self.m_input.rfind('\n', 0, lexpos) 54 | column = lexpos - pos 55 | else: 56 | lineno = None 57 | column = None 58 | return lineno, column 59 | 60 | def t_ANY_error(self, t): 61 | err = self.exception() 62 | msg = 'illegal token' 63 | if self.m_fname: 64 | err.fname = self.m_fname 65 | msg += ' in file %s' % self.m_fname 66 | lineno, column = self._position(t) 67 | if lineno is not None and column is not None: 68 | msg += ' at %d:%d' % (lineno, column) 69 | err.lineno = lineno 70 | err.column = column 71 | err.message = msg 72 | raise err 73 | 74 | def p_error(self, p): 75 | err = self.exception() 76 | msg = 'syntax error' 77 | if self.m_fname: 78 | err.fname = self.m_fname 79 | msg += ' in file %s' % self.m_fname 80 | lineno, column = self._position(p) 81 | if lineno is not None and column is not None: 82 | msg += ' at %d:%d' % (lineno, column) 83 | err.lineno = lineno 84 | err.column = column 85 | err.message = msg 86 | raise err 87 | -------------------------------------------------------------------------------- /lib/ad/core/test/test_locate.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import math 10 | import signal 11 | 12 | from ad.test.base import BaseTest 13 | from ad.core.locate import Locator 14 | from threading import Timer 15 | 16 | 17 | class SRV(object): 18 | """SRV record for Locator testing.""" 19 | 20 | def __init__(self, priority=0, weight=100, target=None, port=None): 21 | self.priority = priority 22 | self.weight = weight 23 | self.target = target 24 | self.port = port 25 | 26 | 27 | class TestLocator(BaseTest): 28 | """Test suite for Locator.""" 29 | 30 | def test_simple(self): 31 | self.require(ad_user=True) 32 | domain = self.domain() 33 | loc = Locator() 34 | result = loc.locate_many(domain) 35 | assert len(result) > 0 36 | result = loc.locate_many(domain, role='gc') 37 | assert len(result) > 0 38 | result = loc.locate_many(domain, role='pdc') 39 | assert len(result) == 1 40 | 41 | def test_network_failure(self): 42 | self.require(ad_user=True, local_admin=True, firewall=True) 43 | domain = self.domain() 44 | loc = Locator() 45 | # Block outgoing DNS and CLDAP traffic and enable it after 3 seconds. 46 | # Locator should be able to handle this. 47 | self.remove_network_blocks() 48 | self.block_outgoing_traffic('tcp', 53) 49 | self.block_outgoing_traffic('udp', 53) 50 | self.block_outgoing_traffic('udp', 389) 51 | t = Timer(3, self.remove_network_blocks); t.start() 52 | result = loc.locate_many(domain) 53 | assert len(result) > 0 54 | 55 | def test_order_dns_srv_priority(self): 56 | srv = [ SRV(10), SRV(0), SRV(10), SRV(20), SRV(100), SRV(5) ] 57 | loc = Locator() 58 | result = loc._order_dns_srv(srv) 59 | prio = [ res.priority for res in result ] 60 | sorted = prio[:] 61 | sorted.sort() 62 | assert prio == sorted 63 | 64 | def test_order_dns_srv_weight(self): 65 | n = 10000 66 | w = (100, 50, 25) 67 | sumw = sum(w) 68 | count = {} 69 | for x in w: 70 | count[x] = 0 71 | loc = Locator() 72 | srv = [ SRV(0, x) for x in w ] 73 | for i in range(n): 74 | res = loc._order_dns_srv(srv) 75 | count[res[0].weight] += 1 76 | print count 77 | 78 | def stddev(n, p): 79 | # standard deviation of binomial distribution 80 | return math.sqrt(n*p*(1-p)) 81 | 82 | for x in w: 83 | p = float(x)/sumw 84 | # 6 sigma this gives a 1 per 100 million chance of wrongly 85 | # asserting an error here. 86 | assert abs(count[x] - n*p) < 6 * stddev(n, p) 87 | 88 | def test_detect_site(self): 89 | self.require(ad_user=True) 90 | loc = Locator() 91 | domain = self.domain() 92 | site = loc._detect_site(domain) 93 | assert site is not None 94 | -------------------------------------------------------------------------------- /test.conf.example: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | 9 | [test] 10 | 11 | # The following setting can enable "expensive tests". Expensive tests are tests 12 | # that can take a significant amount of time to complete. 13 | expensive_tests = 1 14 | 15 | # Some test in the Python-AD test suite require an Active Directory. These tests 16 | # are disabled by default and can be enabled below. A great way of doing AD 17 | # tests is to install an AD forest in one or multiple virtual machines on the 18 | # current system, and connect the systems to a virtual network that is shared 19 | # with the host. Be sure to enable DNS services in the forest. Then add a line 20 | # to "/etc/resolv.conf" pointing to a DNS server in the new AD forest. This is 21 | # sufficient to start using AD tests. 22 | 23 | # Set to 1 to enable tests that require read access to an active directory. All 24 | # tests that become enabled by this setting are non-intrusive. 25 | readonly_ad_tests = 0 26 | 27 | # Change to the domain that is to be used for AD tests. 28 | domain = freeadi.org 29 | 30 | # The name of a non-privileges account in AD. 31 | ad_user_account = 32 | 33 | # The password of the non-privileged account in AD. 34 | ad_user_password = 35 | 36 | 37 | # WARNING !!! WARNING !!! WARNING !!! 38 | # 39 | # Python-AD has support for "intrusive tests". These tests require elevated 40 | # privileges on the current system or in the directory, and may change 41 | # system configurations and update or delete data. 42 | # 43 | # All intrusive tests are disabled by default. They can be enabled by 44 | # settings the corresponding variables to "1" and providing the proper 45 | # password in this configuration file. Two types of intrusive tests exist: 46 | # 47 | # - Intrusive local tests. These tests can modify system configuration 48 | # files, update firewall settings and make other changes. The local root 49 | # password is required to run these tests. 50 | # - Intrusive AD tests. These tests can create / change / delete objects in 51 | # Active Directory. The AD administrator password is required to run 52 | # these tests. 53 | # 54 | # Intrusive tests should be in an environment that is not a production 55 | # environment. A good candidate for such an environment would be a set of 56 | # virtualised host running on a developer workstations which are connected 57 | # to a host-only network. See the section above on AD tests on how to set up 58 | # such an environment. 59 | # 60 | # Intrusive tests are written such that after running a set of tests the 61 | # system will be in a state that allows running the tests again, and so on. 62 | # In particular no restore from backup (or restore to snapshot in a virtual 63 | # environment) is required after running the tests. It is not guaranteed 64 | # however that after a running a set of tests the system will be in the same 65 | # state as it was before the tests. This is the reason that intrusive tests 66 | # should never be run in a production environment. 67 | 68 | # Set to 1 to enable tests that require local admin (root) access, 0 to 69 | # disable. 70 | intrusive_local_tests = 0 71 | 72 | # The local admin account 73 | local_admin_account = root 74 | 75 | # The password of the local admin account 76 | local_admin_password = 77 | 78 | # Set to 1 to enable tests that require AD administrator access, 0 to 79 | # disable. 80 | intrusive_ad_tests = 0 81 | 82 | # The AD administrator account 83 | ad_admin_account = Administrator 84 | 85 | # The AD administrator password 86 | ad_admin_password = 87 | -------------------------------------------------------------------------------- /lib/ad/protocol/ldapfilter.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import re 10 | from ad.util.parser import Parser as PLYParser 11 | 12 | 13 | class Error(Exception): 14 | """LDAP Filter exception""" 15 | 16 | 17 | class AND(object): 18 | 19 | def __init__(self, *terms): 20 | self.terms = terms 21 | 22 | class OR(object): 23 | 24 | def __init__(self, *terms): 25 | self.terms = terms 26 | 27 | class NOT(object): 28 | 29 | def __init__(self, term): 30 | self.term = term 31 | 32 | class EQUALS(object): 33 | 34 | def __init__(self, type, value): 35 | self.type = type 36 | self.value = value 37 | 38 | class LTE(object): 39 | 40 | def __init__(self, type, value): 41 | self.type = type 42 | self.value = value 43 | 44 | class GTE(object): 45 | 46 | def __init__(self, type, value): 47 | self.type = type 48 | self.value = value 49 | 50 | class APPROX(object): 51 | 52 | def __init__(self, type, value): 53 | self.type = type 54 | self.value = value 55 | 56 | class PRESENT(object): 57 | 58 | def __init__(self, type): 59 | self.type = type 60 | 61 | 62 | class Parser(PLYParser): 63 | """A parser for LDAP filters (see RFC2254). 64 | 65 | The parser is pretty complete. Currently lacking are substring matches and 66 | extensible matches. 67 | """ 68 | 69 | exception = Error 70 | tokens = ('LPAREN', 'RPAREN', 'EQUALS', 'AND', 'OR', 'NOT', 71 | 'LTE','GTE', 'APPROX', 'PRESENT', 'STRING') 72 | 73 | t_LPAREN = r'\(' 74 | t_RPAREN = r'\)' 75 | t_EQUALS = r'=' 76 | t_AND = r'&' 77 | t_OR = r'\|' 78 | t_NOT = r'!' 79 | t_LTE = r'<=' 80 | t_GTE = r'>=' 81 | t_APPROX = r'~=' 82 | t_PRESENT = r'=\*' 83 | 84 | re_escape = re.compile(r'\\([0-9a-fA-F]{2})') 85 | 86 | def _unescape(self, value): 87 | """Unescape a hex encoded string.""" 88 | pos = 0 89 | parts = [] 90 | while True: 91 | mobj = self.re_escape.search(value, pos) 92 | if not mobj: 93 | parts.append(value[pos:]) 94 | break 95 | parts.append(value[pos:mobj.start()]) 96 | ch = chr(int(mobj.group(1), 16)) 97 | parts.append(ch) 98 | pos = mobj.end() 99 | result = ''.join(parts) 100 | return result 101 | 102 | def t_STRING(self, t): 103 | r'[^()=&|!<>~*]+' 104 | t.value = self._unescape(t.value) 105 | return t 106 | 107 | def p_filter(self, p): 108 | """filter : LPAREN and RPAREN 109 | | LPAREN or RPAREN 110 | | LPAREN not RPAREN 111 | | LPAREN item RPAREN 112 | """ 113 | p[0] = p[2] 114 | 115 | def p_and(self, p): 116 | 'and : AND filterlist' 117 | p[0] = AND(*p[2]) 118 | 119 | def p_or(self, p): 120 | 'or : OR filterlist' 121 | p[0] = OR(*p[2]) 122 | 123 | def p_not(self, p): 124 | 'not : NOT filter' 125 | p[0] = NOT(p[2]) 126 | 127 | def p_filterlist(self, p): 128 | """filterlist : filter 129 | | filter filterlist 130 | """ 131 | if len(p) == 2: 132 | p[0] = (p[1],) 133 | else: 134 | p[0] = (p[1],) + p[2] 135 | 136 | def p_item(self, p): 137 | """item : STRING EQUALS STRING 138 | | STRING LTE STRING 139 | | STRING GTE STRING 140 | | STRING APPROX STRING 141 | | STRING PRESENT 142 | """ 143 | if p[2] == '=': 144 | p[0] = EQUALS(p[1], p[3]) 145 | elif p[2] == '<=': 146 | p[0] = LTE(p[1], p[3]) 147 | elif p[2] == '>=': 148 | p[0] = GTE(p[1], p[3]) 149 | elif p[2] == '~=': 150 | p[0] = APPROX(p[1], p[3]) 151 | elif p[2] == '=*': 152 | p[0] = PRESENT(p[1]) 153 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/test_ldapfilter.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | from nose.tools import assert_raises 10 | from ad.protocol import ldapfilter 11 | 12 | 13 | class TestLDAPFilterParser(object): 14 | """Test suite for ad.protocol.ldapfilter.""" 15 | 16 | def test_equals(self): 17 | filt = '(type=value)' 18 | parser = ldapfilter.Parser() 19 | res = parser.parse(filt) 20 | assert isinstance(res, ldapfilter.EQUALS) 21 | assert res.type == 'type' 22 | assert res.value == 'value' 23 | 24 | def test_and(self): 25 | filt = '(&(type=value)(type2=value2))' 26 | parser = ldapfilter.Parser() 27 | res = parser.parse(filt) 28 | assert isinstance(res, ldapfilter.AND) 29 | assert len(res.terms) == 2 30 | assert isinstance(res.terms[0], ldapfilter.EQUALS) 31 | assert isinstance(res.terms[1], ldapfilter.EQUALS) 32 | 33 | def test_and_multi_term(self): 34 | filt = '(&(type=value)(type2=value2)(type3=value3))' 35 | parser = ldapfilter.Parser() 36 | res = parser.parse(filt) 37 | assert isinstance(res, ldapfilter.AND) 38 | assert len(res.terms) == 3 39 | assert isinstance(res.terms[0], ldapfilter.EQUALS) 40 | assert isinstance(res.terms[1], ldapfilter.EQUALS) 41 | assert isinstance(res.terms[2], ldapfilter.EQUALS) 42 | 43 | def test_or(self): 44 | filt = '(|(type=value)(type2=value2))' 45 | parser = ldapfilter.Parser() 46 | res = parser.parse(filt) 47 | assert isinstance(res, ldapfilter.OR) 48 | assert len(res.terms) == 2 49 | assert isinstance(res.terms[0], ldapfilter.EQUALS) 50 | assert isinstance(res.terms[1], ldapfilter.EQUALS) 51 | 52 | def test_or_multi_term(self): 53 | filt = '(|(type=value)(type2=value2)(type3=value3))' 54 | parser = ldapfilter.Parser() 55 | res = parser.parse(filt) 56 | assert isinstance(res, ldapfilter.OR) 57 | assert len(res.terms) == 3 58 | assert isinstance(res.terms[0], ldapfilter.EQUALS) 59 | assert isinstance(res.terms[1], ldapfilter.EQUALS) 60 | assert isinstance(res.terms[2], ldapfilter.EQUALS) 61 | 62 | def test_not(self): 63 | filt = '(!(type=value))' 64 | parser = ldapfilter.Parser() 65 | res = parser.parse(filt) 66 | assert isinstance(res, ldapfilter.NOT) 67 | assert isinstance(res.term, ldapfilter.EQUALS) 68 | 69 | def test_lte(self): 70 | filt = '(type<=value)' 71 | parser = ldapfilter.Parser() 72 | res = parser.parse(filt) 73 | assert isinstance(res, ldapfilter.LTE) 74 | assert res.type == 'type' 75 | assert res.value == 'value' 76 | 77 | def test_gte(self): 78 | filt = '(type>=value)' 79 | parser = ldapfilter.Parser() 80 | res = parser.parse(filt) 81 | assert isinstance(res, ldapfilter.GTE) 82 | assert res.type == 'type' 83 | assert res.value == 'value' 84 | 85 | def test_approx(self): 86 | filt = '(type~=value)' 87 | parser = ldapfilter.Parser() 88 | res = parser.parse(filt) 89 | assert isinstance(res, ldapfilter.APPROX) 90 | assert res.type == 'type' 91 | assert res.value == 'value' 92 | 93 | def test_present(self): 94 | filt = '(type=*)' 95 | parser = ldapfilter.Parser() 96 | res = parser.parse(filt) 97 | assert isinstance(res, ldapfilter.PRESENT) 98 | assert res.type == 'type' 99 | 100 | def test_escape(self): 101 | filt = r'(type=\5c\00\2a)' 102 | parser = ldapfilter.Parser() 103 | res = parser.parse(filt) 104 | assert res.value == '\\\x00*' 105 | 106 | def test_error_incomplete_term(self): 107 | parser = ldapfilter.Parser() 108 | filt = '(' 109 | assert_raises(ldapfilter.Error, parser.parse, filt) 110 | filt = '(type' 111 | assert_raises(ldapfilter.Error, parser.parse, filt) 112 | filt = '(type=' 113 | assert_raises(ldapfilter.Error, parser.parse, filt) 114 | filt = '(type=)' 115 | assert_raises(ldapfilter.Error, parser.parse, filt) 116 | 117 | def test_error_not_multi_term(self): 118 | parser = ldapfilter.Parser() 119 | filt = '(!(type=value)(type2=value2))' 120 | assert_raises(ldapfilter.Error, parser.parse, filt) 121 | 122 | def test_error_illegal_operator(self): 123 | parser = ldapfilter.Parser() 124 | filt = '($(type=value)(type2=value2))' 125 | assert_raises(ldapfilter.Error, parser.parse, filt) 126 | 127 | def test_error_illegal_character(self): 128 | parser = ldapfilter.Parser() 129 | filt = '(type=val*e)' 130 | assert_raises(ldapfilter.Error, parser.parse, filt) 131 | -------------------------------------------------------------------------------- /lib/ad/core/test/test_creds.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os 10 | import pexpect 11 | 12 | from ad.test.base import BaseTest 13 | from ad.core.creds import Creds as ADCreds 14 | from ad.core.object import instance, activate 15 | 16 | 17 | class TestCreds(BaseTest): 18 | """Test suite for ad.core.creds.""" 19 | 20 | def test_acquire_password(self): 21 | self.require(ad_user=True) 22 | domain = self.domain() 23 | creds = ADCreds(domain) 24 | principal = self.ad_user_account() 25 | password = self.ad_user_password() 26 | creds.acquire(principal, password) 27 | principal = '%s@%s' % (principal, domain) 28 | assert creds.principal().lower() == principal.lower() 29 | child = pexpect.spawn('klist') 30 | pattern = '.*krbtgt/%s@%s' % (domain.upper(), domain.upper()) 31 | assert child.expect([pattern]) == 0 32 | 33 | def test_acquire_keytab(self): 34 | self.require(ad_user=True) 35 | domain = self.domain() 36 | creds = ADCreds(domain) 37 | principal = self.ad_user_account() 38 | password = self.ad_user_password() 39 | creds.acquire(principal, password) 40 | os.environ['PATH'] = '/usr/kerberos/sbin:/usr/kerberos/bin:%s' % \ 41 | os.environ['PATH'] 42 | fullprinc = creds.principal() 43 | child = pexpect.spawn('kvno %s' % fullprinc) 44 | child.expect('kvno =') 45 | kvno = int(child.readline()) 46 | child.expect(pexpect.EOF) 47 | child = pexpect.spawn('ktutil') 48 | child.expect('ktutil:') 49 | child.sendline('addent -password -p %s -k %d -e rc4-hmac' % 50 | (fullprinc, kvno)) 51 | child.expect('Password for.*:') 52 | child.sendline(password) 53 | child.expect('ktutil:') 54 | keytab = self.tempfile(remove=True) 55 | child.sendline('wkt %s' % keytab) 56 | child.expect('ktutil:') 57 | child.sendline('quit') 58 | child.expect(pexpect.EOF) 59 | creds.release() 60 | creds.acquire(principal, keytab=keytab) 61 | child = pexpect.spawn('klist') 62 | pattern = '.*krbtgt/%s@%s' % (domain.upper(), domain.upper()) 63 | assert child.expect([pattern]) == 0 64 | 65 | def test_load(self): 66 | self.require(ad_user=True) 67 | domain = self.domain().upper() 68 | principal = '%s@%s' % (self.ad_user_account(), domain) 69 | self.acquire_credentials(principal, self.ad_user_password()) 70 | creds = ADCreds(domain) 71 | creds.load() 72 | assert creds.principal().lower() == principal.lower() 73 | ccache, princ, creds = self.list_credentials() 74 | assert princ.lower() == principal.lower() 75 | assert len(creds) > 0 76 | assert creds[0] == 'krbtgt/%s@%s' % (domain, domain) 77 | 78 | def test_acquire_multi(self): 79 | self.require(ad_user=True) 80 | domain = self.domain() 81 | principal = self.ad_user_account() 82 | password = self.ad_user_password() 83 | creds1 = ADCreds(domain) 84 | creds1.acquire(principal, password) 85 | ccache1 = creds1._ccache_name() 86 | config1 = creds1._config_name() 87 | assert ccache1 == os.environ['KRB5CCNAME'] 88 | assert config1 == os.environ['KRB5_CONFIG'] 89 | creds2 = ADCreds(domain) 90 | creds2.acquire(principal, password) 91 | ccache2 = creds2._ccache_name() 92 | config2 = creds2._config_name() 93 | assert ccache2 == os.environ['KRB5CCNAME'] 94 | assert config2 == os.environ['KRB5_CONFIG'] 95 | assert ccache1 != ccache2 96 | assert config1 != config2 97 | activate(creds1) 98 | assert os.environ['KRB5CCNAME'] == ccache1 99 | assert os.environ['KRB5_CONFIG'] == config1 100 | activate(creds2) 101 | assert os.environ['KRB5CCNAME'] == ccache2 102 | assert os.environ['KRB5_CONFIG'] == config2 103 | 104 | def test_release_multi(self): 105 | self.require(ad_user=True) 106 | domain = self.domain() 107 | principal = self.ad_user_account() 108 | password = self.ad_user_password() 109 | ccorig = os.environ.get('KRB5CCNAME') 110 | cforig = os.environ.get('KRB5_CONFIG') 111 | creds1 = ADCreds(domain) 112 | creds1.acquire(principal, password) 113 | ccache1 = creds1._ccache_name() 114 | config1 = creds1._config_name() 115 | creds2 = ADCreds(domain) 116 | creds2.acquire(principal, password) 117 | ccache2 = creds2._ccache_name() 118 | config2 = creds2._config_name() 119 | creds1.release() 120 | assert os.environ['KRB5CCNAME'] == ccache2 121 | assert os.environ['KRB5_CONFIG'] == config2 122 | creds2.release() 123 | assert os.environ.get('KRB5CCNAME') == ccorig 124 | assert os.environ.get('KRB5_CONFIG') == cforig 125 | 126 | def test_cleanup_files(self): 127 | self.require(ad_user=True) 128 | domain = self.domain() 129 | principal = self.ad_user_account() 130 | password = self.ad_user_password() 131 | creds = ADCreds(domain) 132 | creds.acquire(principal, password) 133 | ccache = creds._ccache_name() 134 | config = creds._config_name() 135 | assert os.access(ccache, os.R_OK) 136 | assert os.access(config, os.R_OK) 137 | creds.release() 138 | assert not os.access(ccache, os.R_OK) 139 | assert not os.access(config, os.R_OK) 140 | 141 | def test_cleanup_environment(self): 142 | self.require(ad_user=True) 143 | domain = self.domain() 144 | principal = self.ad_user_account() 145 | password = self.ad_user_password() 146 | ccorig = os.environ.get('KRB5CCNAME') 147 | cforig = os.environ.get('KRB5_CONFIG') 148 | creds = ADCreds(domain) 149 | creds.acquire(principal, password) 150 | ccache = creds._ccache_name() 151 | config = creds._config_name() 152 | assert ccache != ccorig 153 | assert config != cforig 154 | creds.release() 155 | assert os.environ.get('KRB5CCNAME') == ccorig 156 | assert os.environ.get('KRB5_CONFIG') == cforig 157 | -------------------------------------------------------------------------------- /lib/ad/protocol/ldap.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | from ad.protocol import asn1 10 | from ad.protocol import ldapfilter 11 | 12 | 13 | SCOPE_BASE = 0 14 | SCOPE_ONELEVEL = 1 15 | SCOPE_SUBTREE = 2 16 | 17 | DEREF_NEVER = 0 18 | DEREF_IN_SEARCHING = 1 19 | DEREF_FINDING_BASE_OBJ = 2 20 | DEREF_ALWAYS = 3 21 | 22 | 23 | class Error(Exception): 24 | """LDAP Error""" 25 | 26 | 27 | class Client(object): 28 | """LDAP client.""" 29 | 30 | def _encode_filter(self, encoder, filter): 31 | """Encode a parsed LDAP filter using `encoder'.""" 32 | if isinstance(filter, ldapfilter.AND): 33 | encoder.enter(0, asn1.ClassContext) 34 | for term in filter.terms: 35 | self._encode_filter(encoder, term) 36 | encoder.leave() 37 | elif isinstance(filter, ldapfilter.OR): 38 | encoder.enter(1, asn1.ClassContext) 39 | for term in filter.terms: 40 | self._encode_filter(encoder, term) 41 | encoder.leave() 42 | elif isinstance(filter, ldapfilter.NOT): 43 | encoder.enter(2, asn1.ClassContext) 44 | self._encode_filter(encoder, term) 45 | encoder.leave() 46 | elif isinstance(filter, ldapfilter.EQUALS): 47 | encoder.enter(3, asn1.ClassContext) 48 | encoder.write(filter.type) 49 | encoder.write(filter.value) 50 | encoder.leave() 51 | elif isinstance(filter, ldapfilter.LTE): 52 | encoder.enter(5, asn1.ClassContext) 53 | encoder.write(filter.type) 54 | encoder.write(filter.value) 55 | encoder.leave() 56 | elif isinstance(filter, ldapfilter.GTE): 57 | encoder.enter(6, asn1.ClassContext) 58 | encoder.write(filter.type) 59 | encoder.write(filter.value) 60 | encoder.leave() 61 | elif isinstance(filter, ldapfilter.PRESENT): 62 | encoder.enter(7, asn1.ClassContext) 63 | encoder.write(filter.type) 64 | encoder.leave() 65 | elif isinstance(filter, ldapfilter.APPROX): 66 | encoder.enter(8, asn1.ClassContext) 67 | encoder.write(filter.type) 68 | encoder.write(filter.value) 69 | encoder.leave() 70 | 71 | def create_search_request(self, dn, filter=None, attrs=None, scope=None, 72 | sizelimit=None, timelimit=None, deref=None, 73 | typesonly=None, msgid=None): 74 | """Create a search request. This only supports a very simple AND 75 | filter.""" 76 | if filter is None: 77 | filter = '(objectClass=*)' 78 | if attrs is None: 79 | attrs = [] 80 | if scope is None: 81 | scope = SCOPE_SUBTREE 82 | if sizelimit is None: 83 | sizelimit = 0 84 | if timelimit is None: 85 | timelimit = 0 86 | if deref is None: 87 | deref = DEREF_NEVER 88 | if typesonly is None: 89 | typesonly = False 90 | if msgid is None: 91 | msgid = 1 92 | parser = ldapfilter.Parser() 93 | parsed = parser.parse(filter) 94 | encoder = asn1.Encoder() 95 | encoder.start() 96 | encoder.enter(asn1.Sequence) # LDAPMessage 97 | encoder.write(msgid) 98 | encoder.enter(3, asn1.ClassApplication) # SearchRequest 99 | encoder.write(dn) 100 | encoder.write(scope, asn1.Enumerated) 101 | encoder.write(deref, asn1.Enumerated) 102 | encoder.write(sizelimit) 103 | encoder.write(timelimit) 104 | encoder.write(typesonly, asn1.Boolean) 105 | self._encode_filter(encoder, parsed) 106 | encoder.enter(asn1.Sequence) # attributes 107 | for attr in attrs: 108 | encoder.write(attr) 109 | encoder.leave() # end of attributes 110 | encoder.leave() # end of SearchRequest 111 | encoder.leave() # end of LDAPMessage 112 | result = encoder.output() 113 | return result 114 | 115 | def parse_message_header(self, buffer): 116 | """Parse an LDAP header and return the tuple (messageid, 117 | protocolOp).""" 118 | decoder = asn1.Decoder() 119 | decoder.start(buffer) 120 | self._check_tag(decoder.peek(), asn1.Sequence) 121 | decoder.enter() 122 | self._check_tag(decoder.peek(), asn1.Integer) 123 | msgid = decoder.read()[1] 124 | tag = decoder.peek() 125 | self._check_tag(tag, None, asn1.TypeConstructed, asn1.ClassApplication) 126 | op = tag[0] 127 | return (msgid, op) 128 | 129 | def parse_search_result(self, buffer): 130 | """Parse an LDAP search result. 131 | 132 | This function returns a list of search result. Each entry in the list 133 | is a (msgid, dn, attrs) tuple. attrs is a dictionary with LDAP types 134 | as keys and a list of attribute values as its values. 135 | """ 136 | decoder = asn1.Decoder() 137 | decoder.start(buffer) 138 | messages = [] 139 | while True: 140 | tag = decoder.peek() 141 | if tag is None: 142 | break 143 | self._check_tag(tag, asn1.Sequence) 144 | decoder.enter() # enter LDAPMessage 145 | self._check_tag(decoder.peek(), asn1.Integer) 146 | msgid = decoder.read()[1] # messageID 147 | tag = decoder.peek() 148 | self._check_tag(tag, (4,5), asn1.TypeConstructed, asn1.ClassApplication) 149 | if tag[0] == 5: 150 | break 151 | decoder.enter() # SearchResultEntry 152 | self._check_tag(decoder.peek(), asn1.OctetString) 153 | dn = decoder.read()[1] # objectName 154 | self._check_tag(decoder.peek(), asn1.Sequence) 155 | decoder.enter() # enter attributes 156 | attrs = {} 157 | while True: 158 | tag = decoder.peek() 159 | if tag is None: 160 | break 161 | self._check_tag(tag, asn1.Sequence) 162 | decoder.enter() # one attribute 163 | self._check_tag(decoder.peek(), asn1.OctetString) 164 | name = decoder.read()[1] # type 165 | self._check_tag(decoder.peek(), asn1.Set) 166 | decoder.enter() # vals 167 | values = [] 168 | while True: 169 | tag = decoder.peek() 170 | if tag is None: 171 | break 172 | self._check_tag(tag, asn1.OctetString) 173 | values.append(decoder.read()[1]) 174 | attrs[name] = values 175 | decoder.leave() # leave vals 176 | decoder.leave() # leave attribute 177 | decoder.leave() # leave attributes 178 | messages.append((msgid, dn, attrs)) 179 | return messages 180 | 181 | def _check_tag(self, tag, id, typ=None, cls=None): 182 | """Ensure that `tag' matches with `id', `typ' and `syntax'.""" 183 | if cls is None: 184 | cls = asn1.ClassUniversal 185 | if typ is None: 186 | if id in (asn1.Sequence, asn1.Set): 187 | typ = asn1.TypeConstructed 188 | else: 189 | typ = asn1.TypePrimitive 190 | if isinstance(id, tuple): 191 | if tag[0] not in id: 192 | raise Error, 'LDAP syntax error' 193 | elif id is not None: 194 | if tag[0] != id: 195 | raise Error, 'LDAP syntax error' 196 | if tag[1] != typ or tag[2] != cls: 197 | raise Error, 'LDAP syntax error' 198 | -------------------------------------------------------------------------------- /lib/ad/test/base.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os 10 | import os.path 11 | import sys 12 | import tempfile 13 | import pexpect 14 | 15 | from nose import SkipTest 16 | from ConfigParser import ConfigParser 17 | from ad.util.log import enable_logging 18 | 19 | 20 | class Error(Exception): 21 | """Test error.""" 22 | 23 | 24 | class BaseTest(object): 25 | """Base class for Python-AD tests.""" 26 | 27 | @classmethod 28 | def setup_class(cls): 29 | config = ConfigParser() 30 | fname = os.environ.get('FREEADI_TEST_CONFIG') 31 | if fname is None: 32 | raise Error, 'Python-AD test configuration file not specified.' 33 | if not os.access(fname, os.R_OK): 34 | raise Error, 'Python-AD test configuration file does not exist.' 35 | config.read(fname) 36 | cls.c_config = config 37 | cls.c_basedir = os.path.dirname(fname) 38 | cls.c_iptables = None 39 | cls.c_tempfiles = [] 40 | enable_logging() 41 | 42 | @classmethod 43 | def teardown_class(cls): 44 | for fname in cls.c_tempfiles: 45 | try: 46 | os.unlink(fname) 47 | except OSError: 48 | pass 49 | cls.c_tempfiles = [] 50 | 51 | def config(self): 52 | return self.c_config 53 | 54 | def _dedent(self, s): 55 | lines = s.splitlines() 56 | for i in range(len(lines)): 57 | lines[i] = lines[i].lstrip() 58 | if lines and not lines[0]: 59 | lines = lines[1:] 60 | if lines and not lines[-1]: 61 | lines = lines[:-1] 62 | return '\n'.join(lines) + '\n' 63 | 64 | def tempfile(self, contents=None, remove=False): 65 | fd, name = tempfile.mkstemp() 66 | if contents: 67 | os.write(fd, self._dedent(contents)) 68 | elif remove: 69 | os.remove(name) 70 | os.close(fd) 71 | self.c_tempfiles.append(name) 72 | return name 73 | 74 | def basedir(self): 75 | return self.c_basedir 76 | 77 | def require(self, ad_user=False, local_admin=False, ad_admin=False, 78 | firewall=False, expensive=False): 79 | if firewall: 80 | local_admin = True 81 | config = self.config() 82 | if ad_user and not config.getboolean('test', 'readonly_ad_tests'): 83 | raise SkipTest, 'test disabled by configuration' 84 | if not config.get('test', 'domain'): 85 | raise SkipTest, 'ad tests enabled but no domain given' 86 | if not config.get('test', 'ad_user_account') or \ 87 | not config.get('test', 'ad_user_password'): 88 | raise SkipTest, 'readonly ad tests enabled but no user/pw given' 89 | if local_admin: 90 | if not config.getboolean('test', 'intrusive_local_tests'): 91 | raise SkipTest, 'test disabled by configuration' 92 | if not config.get('test', 'local_admin_account') or \ 93 | not config.get('test', 'local_admin_password'): 94 | raise SkipTest, 'intrusive local tests enabled but no user/pw given' 95 | if ad_admin: 96 | if not config.getboolean('test', 'intrusive_ad_tests'): 97 | raise SkipTest, 'test disabled by configuration' 98 | if not config.get('test', 'ad_admin_account') or \ 99 | not config.get('test', 'ad_admin_password'): 100 | raise SkipTest, 'intrusive ad tests enabled but no user/pw given' 101 | if firewall and not self._iptables_supported(): 102 | raise SkipTest, 'iptables/conntrack not available' 103 | if expensive and not config.getboolean('test', 'expensive_tests'): 104 | raise SkipTest, 'test disabled by configuration' 105 | 106 | def domain(self): 107 | config = self.config() 108 | domain = config.get('test', 'domain') 109 | return domain 110 | 111 | def ad_user_account(self): 112 | self.require(ad_user=True) 113 | account = self.config().get('test', 'ad_user_account') 114 | return account 115 | 116 | def ad_user_password(self): 117 | self.require(ad_user=True) 118 | password = self.config().get('test', 'ad_user_password') 119 | return password 120 | 121 | def local_admin_account(self): 122 | self.require(local_admin=True) 123 | account = self.config().get('test', 'local_admin_account') 124 | return account 125 | 126 | def local_admin_password(self): 127 | self.require(local_admin=True) 128 | password = self.config().get('test', 'local_admin_password') 129 | return password 130 | 131 | def ad_admin_account(self): 132 | self.require(ad_admin=True) 133 | account = self.config().get('test', 'ad_admin_account') 134 | return account 135 | 136 | def ad_admin_password(self): 137 | self.require(ad_admin=True) 138 | password = self.config().get('test', 'ad_admin_password') 139 | return password 140 | 141 | def execute_as_root(self, command): 142 | self.require(local_admin=True) 143 | child = pexpect.spawn('su -c "%s" %s' % \ 144 | (command, self.local_admin_account())) 145 | child.expect('.*:') 146 | child.sendline(self.local_admin_password()) 147 | child.expect(pexpect.EOF) 148 | assert not child.isalive() 149 | if child.exitstatus != 0: 150 | m = 'Root command exited with status %s' % child.exitstatus 151 | raise Error, m 152 | return child.before 153 | 154 | def acquire_credentials(self, principal, password, ccache=None): 155 | if ccache is None: 156 | ccache = '' 157 | else: 158 | ccache = '-c %s' % ccache 159 | child = pexpect.spawn('kinit %s %s' % (principal, ccache)) 160 | child.expect(':') 161 | child.sendline(password) 162 | child.expect(pexpect.EOF) 163 | assert not child.isalive() 164 | if child.exitstatus != 0: 165 | m = 'Command kinit exited with status %s' % child.exitstatus 166 | raise Error, m 167 | 168 | def list_credentials(self, ccache=None): 169 | if ccache is None: 170 | ccache = '' 171 | child = pexpect.spawn('klist %s' % ccache) 172 | try: 173 | child.expect('Ticket cache: ([a-zA-Z0-9_/.:-]+)\r\n') 174 | except pexpect.EOF: 175 | m = 'Command klist exited with status %s' % child.exitstatus 176 | raise Error, m 177 | ccache = child.match.group(1) 178 | child.expect('Default principal: ([a-zA-Z0-9_/.:@-]+)\r\n') 179 | principal = child.match.group(1) 180 | creds = [] 181 | while True: 182 | i = child.expect(['\r\n', pexpect.EOF, 183 | '\d\d/\d\d/\d\d \d\d:\d\d:\d\d\s+' \ 184 | '\d\d/\d\d/\d\d \d\d:\d\d:\d\d\s+' \ 185 | '([a-zA-Z0-9_/.:@-]+)\r\n']) 186 | if i == 0: 187 | continue 188 | elif i == 1: 189 | break 190 | creds.append(child.match.group(1)) 191 | return ccache, principal, creds 192 | 193 | def _iptables_supported(self): 194 | if self.c_iptables is None: 195 | try: 196 | self.execute_as_root('iptables -L -n') 197 | self.execute_as_root('conntrack -L') 198 | except Error: 199 | self.c_iptables = False 200 | else: 201 | self.c_iptables = True 202 | return self.c_iptables 203 | 204 | def remove_network_blocks(self): 205 | self.require(local_admin=True, firewall=True) 206 | self.execute_as_root('iptables -t nat -F') 207 | self.execute_as_root('conntrack -F') 208 | 209 | def block_outgoing_traffic(self, protocol, port): 210 | """Block outgoing traffic of type `protocol' with destination `port'.""" 211 | self.require(local_admin=True, firewall=True) 212 | # Unfortunately we cannot simply insert a rule like this: -A OUTPUT -m 213 | # udp -p udp--dport 389 -j DROP. If we do this the kernel code will 214 | # be smart and return an error when sending trying to connect or send 215 | # a datagram. In order realistically emulate a network failure we 216 | # instead redirect packets the discard port on localhost. This 217 | # complicates stopping the emulated failure though: merely flushling 218 | # the nat table is not enough. We also need to flush the conntrack 219 | # table that keeps state for NAT'ed connections even after the rule 220 | # that caused the NAT in the first place has been removed. 221 | self.execute_as_root('iptables -t nat -A OUTPUT -m %s -p %s --dport %d' 222 | ' -j DNAT --to-destination 127.0.0.1:9' % 223 | (protocol, protocol, port)) 224 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/test_netlogon.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os.path 10 | import signal 11 | import dns.resolver 12 | 13 | from threading import Timer 14 | from nose.tools import assert_raises 15 | from ad.test.base import BaseTest 16 | from ad.protocol import netlogon 17 | 18 | 19 | class TestDecoder(BaseTest): 20 | """Test suite for netlogon.Decoder.""" 21 | 22 | def decode_uint32(self, buffer, offset): 23 | d = netlogon.Decoder() 24 | d.start(buffer) 25 | d._set_offset(offset) 26 | return d._decode_uint32(), d._offset() 27 | 28 | def test_uint32_simple(self): 29 | s = '\x01\x00\x00\x00' 30 | assert self.decode_uint32(s, 0) == (1, 4) 31 | 32 | def test_uint32_byte_order(self): 33 | s = '\x00\x01\x00\x00' 34 | assert self.decode_uint32(s, 0) == (0x100, 4) 35 | s = '\x00\x00\x01\x00' 36 | assert self.decode_uint32(s, 0) == (0x10000, 4) 37 | s = '\x00\x00\x00\x01' 38 | assert self.decode_uint32(s, 0) == (0x1000000, 4) 39 | 40 | def test_uint32_long(self): 41 | s = '\x00\x00\x00\xff' 42 | assert self.decode_uint32(s, 0) == (0xff000000L, 4) 43 | s = '\xff\xff\xff\xff' 44 | assert self.decode_uint32(s, 0) == (0xffffffffL, 4) 45 | 46 | def test_error_uint32_null_input(self): 47 | s = '' 48 | assert_raises(netlogon.Error, self.decode_uint32, s, 0) 49 | 50 | def test_error_uint32_short_input(self): 51 | s = '\x00' 52 | assert_raises(netlogon.Error, self.decode_uint32, s, 0) 53 | s = '\x00\x00' 54 | assert_raises(netlogon.Error, self.decode_uint32, s, 0) 55 | s = '\x00\x00\x00' 56 | assert_raises(netlogon.Error, self.decode_uint32, s, 0) 57 | 58 | def decode_rfc1035(self, buffer, offset): 59 | d = netlogon.Decoder() 60 | d.start(buffer) 61 | d._set_offset(offset) 62 | return d._decode_rfc1035(), d._offset() 63 | 64 | def test_rfc1035_simple(self): 65 | s = '\x03foo\x00' 66 | assert self.decode_rfc1035(s, 0) == ('foo', 5) 67 | 68 | def test_rfc1035_multi_component(self): 69 | s = '\x03foo\x03bar\x00' 70 | assert self.decode_rfc1035(s, 0) == ('foo.bar', 9) 71 | 72 | def test_rfc1035_pointer(self): 73 | s = '\x03foo\x00\xc0\x00' 74 | assert self.decode_rfc1035(s, 5) == ('foo', 7) 75 | 76 | def test_rfc1035_forward_pointer(self): 77 | s = '\xc0\x02\x03foo\x00' 78 | assert self.decode_rfc1035(s, 0) == ('foo', 2) 79 | 80 | def test_rfc1035_pointer_component(self): 81 | s = '\x03foo\x00\x03bar\xc0\x00' 82 | assert self.decode_rfc1035(s, 5) == ('bar.foo', 11) 83 | 84 | def test_rfc1035_pointer_multi_component(self): 85 | s = '\x03foo\x03bar\x00\x03baz\xc0\x00' 86 | assert self.decode_rfc1035(s, 9) == ('baz.foo.bar', 15) 87 | 88 | def test_rfc1035_pointer_recursive(self): 89 | s = '\x03foo\x00\x03bar\xc0\x00\x03baz\xc0\x05' 90 | assert self.decode_rfc1035(s, 11) == ('baz.bar.foo', 17) 91 | 92 | def test_rfc1035_multi_string(self): 93 | s = '\x03foo\x00\x03bar\x00' 94 | assert self.decode_rfc1035(s, 0) == ('foo', 5) 95 | assert self.decode_rfc1035(s, 5) == ('bar', 10) 96 | 97 | def test_rfc1035_null(self): 98 | s = '\x00' 99 | assert self.decode_rfc1035(s, 0) == ('', 1) 100 | 101 | def test_error_rfc1035_null_input(self): 102 | s = '' 103 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 104 | 105 | def test_error_rfc1035_missing_tag(self): 106 | s = '\x03foo' 107 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 108 | 109 | def test_error_rfc1035_truncated_input(self): 110 | s = '\x04foo' 111 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 112 | 113 | def test_error_rfc1035_pointer_overflow(self): 114 | s = '\xc0\x03' 115 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 116 | 117 | def test_error_rfc1035_cyclic_pointer(self): 118 | s = '\xc0\x00' 119 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 120 | s = '\x03foo\xc0\x06\x03bar\xc0\x0c\x03baz\xc0\x00' 121 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 122 | 123 | def test_error_rfc1035_illegal_tags(self): 124 | s = '\x80' + 0x80 * 'a' + '\x00' 125 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 126 | s = '\x40' + 0x40 * 'a' + '\x00' 127 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 128 | 129 | def test_error_rfc1035_half_pointer(self): 130 | s = '\xc0' 131 | assert_raises(netlogon.Error, self.decode_rfc1035, s, 0) 132 | 133 | def test_io_byte(self): 134 | d = netlogon.Decoder() 135 | s = 'foo' 136 | d.start(s) 137 | assert d._read_byte() == 'f' 138 | assert d._read_byte() == 'o' 139 | assert d._read_byte() == 'o' 140 | 141 | def test_io_bytes(self): 142 | d = netlogon.Decoder() 143 | s = 'foo' 144 | d.start(s) 145 | assert d._read_bytes(3) == 'foo' 146 | 147 | def test_error_io_byte(self): 148 | d = netlogon.Decoder() 149 | s = 'foo' 150 | d.start(s) 151 | for i in range(3): 152 | d._read_byte() 153 | assert_raises(netlogon.Error, d._read_byte) 154 | 155 | def test_error_io_bytes(self): 156 | d = netlogon.Decoder() 157 | s = 'foo' 158 | d.start(s) 159 | assert_raises(netlogon.Error, d._read_bytes, 4) 160 | 161 | def test_error_io_bounds(self): 162 | d = netlogon.Decoder() 163 | s = 'foo' 164 | d.start(s) 165 | d._set_offset(4) 166 | assert_raises(netlogon.Error, d._read_byte) 167 | assert_raises(netlogon.Error, d._read_bytes, 4) 168 | 169 | def test_error_negative_offset(self): 170 | d = netlogon.Decoder() 171 | s = 'foo' 172 | d.start(s) 173 | assert_raises(netlogon.Error, d._set_offset, -1) 174 | 175 | def test_error_io_type(self): 176 | d = netlogon.Decoder() 177 | assert_raises(netlogon.Error, d.start, 1) 178 | assert_raises(netlogon.Error, d.start, 1L) 179 | assert_raises(netlogon.Error, d.start, ()) 180 | assert_raises(netlogon.Error, d.start, []) 181 | assert_raises(netlogon.Error, d.start, {}) 182 | assert_raises(netlogon.Error, d.start, u'test') 183 | 184 | def test_real_packet(self): 185 | fname = os.path.join(self.basedir(), 'lib/ad/protocol/test', 186 | 'netlogon.bin') 187 | fin = file(fname) 188 | buf = fin.read() 189 | fin.close() 190 | dec = netlogon.Decoder() 191 | dec.start(buf) 192 | res = dec.parse() 193 | assert res.forest == 'freeadi.org' 194 | assert res.domain == 'freeadi.org' 195 | assert res.client_site == 'Default-First-Site' 196 | assert res.server_site == 'Test-Site' 197 | 198 | def test_error_short_input(self): 199 | buf = 'x' * 24 200 | dec = netlogon.Decoder() 201 | dec.start(buf) 202 | assert_raises(netlogon.Error, dec.parse) 203 | 204 | 205 | class TestClient(BaseTest): 206 | """Test suite for netlogon.Client.""" 207 | 208 | def test_simple(self): 209 | self.require(ad_user=True) 210 | domain = self.domain() 211 | client = netlogon.Client() 212 | answer = dns.resolver.query('_ldap._tcp.%s' % domain, 'SRV') 213 | addrs = [ (ans.target.to_text(), ans.port) for ans in answer ] 214 | names = [ ans.target.to_text().rstrip('.') for ans in answer ] 215 | for addr in addrs: 216 | client.query(addr, domain) 217 | result = client.call() 218 | assert len(result) == len(addrs) # assume retries are succesful 219 | for res in result: 220 | assert res.type in (23,) 221 | assert res.flags & netlogon.SERVER_LDAP 222 | assert res.flags & netlogon.SERVER_KDC 223 | assert res.flags & netlogon.SERVER_WRITABLE 224 | assert len(res.domain_guid) == 16 225 | assert len(res.forest) > 0 226 | assert res.domain == domain 227 | assert res.hostname in names 228 | assert len(res.netbios_domain) > 0 229 | assert len(res.netbios_hostname) > 0 230 | assert len(res.client_site) > 0 231 | assert len(res.server_site) > 0 232 | assert (res.q_hostname, res.q_port) in addrs 233 | assert res.q_domain.lower() == domain.lower() 234 | assert res.q_timing >= 0.0 235 | 236 | def test_network_failure(self): 237 | self.require(ad_user=True, local_admin=True, firewall=True) 238 | domain = self.domain() 239 | client = netlogon.Client() 240 | answer = dns.resolver.query('_ldap._tcp.%s' % domain, 'SRV') 241 | addrs = [ (ans.target.to_text(), ans.port) for ans in answer ] 242 | for addr in addrs: 243 | client.query(addr, domain) 244 | # Block CLDAP traffic and enable it after 3 seconds. Because 245 | # NetlogonClient is retrying, it should be succesfull. 246 | self.remove_network_blocks() 247 | self.block_outgoing_traffic('udp', 389) 248 | t = Timer(3, self.remove_network_blocks); t.start() 249 | result = client.call() 250 | assert len(result) == len(addrs) 251 | -------------------------------------------------------------------------------- /lib/ad/protocol/netlogon.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import time 10 | import errno 11 | import socket 12 | import select 13 | import random 14 | 15 | from ad.util import misc 16 | from ad.protocol import asn1, ldap 17 | 18 | 19 | SERVER_PDC = 0x1 20 | SERVER_GC = 0x4 21 | SERVER_LDAP = 0x8 22 | SERVER_DS = 0x10 23 | SERVER_KDC = 0x20 24 | SERVER_TIMESERV = 0x40 25 | SERVER_CLOSEST = 0x80 26 | SERVER_WRITABLE = 0x100 27 | SERVER_GOOD_TIMESERV = 0x200 28 | 29 | 30 | class Error(Exception): 31 | """Netlogon error.""" 32 | 33 | 34 | class Reply(object): 35 | """The result of a NetLogon RPC.""" 36 | 37 | def __init__(self, **kwargs): 38 | """Constructor.""" 39 | for key in kwargs: 40 | setattr(self, key, kwargs[key]) 41 | 42 | 43 | class Decoder(object): 44 | """Netlogon decoder.""" 45 | 46 | def start(self, buffer): 47 | """Start decoding `buffer'.""" 48 | self._set_buffer(buffer) 49 | self._set_offset(0) 50 | 51 | def parse(self): 52 | """Parse a netlogon reply.""" 53 | type = self._decode_uint32() 54 | flags = self._decode_uint32() 55 | domain_guid = self._read_bytes(16) 56 | forest = self._decode_rfc1035() 57 | domain = self._decode_rfc1035() 58 | hostname = self._decode_rfc1035() 59 | netbios_domain = self._decode_rfc1035() 60 | netbios_hostname = self._decode_rfc1035() 61 | user = self._decode_rfc1035() 62 | client_site = self._decode_rfc1035() 63 | server_site = self._decode_rfc1035() 64 | return Reply(type=type, flags=flags, domain_guid=domain_guid, 65 | forest=forest, domain=domain, hostname=hostname, 66 | netbios_domain=netbios_domain, 67 | netbios_hostname=netbios_hostname, user=user, 68 | client_site=client_site, server_site=server_site) 69 | 70 | def _decode_rfc1035(self, _pointer=False): 71 | """Decompress an RFC1035 (section 4.1.4) compressed string.""" 72 | result = [] 73 | if _pointer == False: 74 | _pointer = [] 75 | while True: 76 | tag = ord(self._read_byte()) 77 | if tag == 0: 78 | break 79 | elif tag & 0xc0 == 0xc0: 80 | byte = self._read_byte() 81 | ptr = ((tag & ~0xc0) << 8) + ord(byte) 82 | if ptr in _pointer: 83 | raise Error, 'Cyclic pointer' 84 | _pointer.append(ptr) 85 | saved, self.m_offset = self.m_offset, ptr 86 | result.append(self._decode_rfc1035(_pointer)) 87 | self.m_offset = saved 88 | break 89 | elif tag & 0xc0: 90 | raise Error, 'Illegal tag' 91 | else: 92 | s = self._read_bytes(tag) 93 | result.append(s) 94 | result = '.'.join(result) 95 | return result 96 | 97 | def _try_convert_int(self, value): 98 | """Try to convert `value' to an integer.""" 99 | try: 100 | value = int(value) 101 | except OverflowError: 102 | pass 103 | return value 104 | 105 | def _decode_uint32(self): 106 | """Decode a 32-bit unsigned little endian integer from the current 107 | offset.""" 108 | value = 0L 109 | for i in range(4): 110 | byte = self._read_byte() 111 | value |= (long(ord(byte)) << i*8) 112 | value = self._try_convert_int(value) 113 | return value 114 | 115 | def _offset(self): 116 | """Return the current offset.""" 117 | return self.m_offset 118 | 119 | def _set_offset(self, offset): 120 | """Set the current decoding offset.""" 121 | if offset < 0: 122 | raise Error, 'Offset must be positive.' 123 | self.m_offset = offset 124 | 125 | def _buffer(self): 126 | """Return the current buffer.""" 127 | return self.m_buffer 128 | 129 | def _set_buffer(self, buffer): 130 | """Set the current buffer.""" 131 | if not isinstance(buffer, str): 132 | raise Error, 'Buffer must be plain string.' 133 | self.m_buffer = buffer 134 | 135 | def _read_byte(self, offset=None): 136 | """Read a single byte from the input.""" 137 | if offset is None: 138 | offset = self.m_offset 139 | update_offset = True 140 | else: 141 | update_offset = False 142 | if offset >= len(self.m_buffer): 143 | raise Error, 'Premature end of input.' 144 | byte = self.m_buffer[offset] 145 | if update_offset: 146 | self.m_offset += 1 147 | return byte 148 | 149 | def _read_bytes(self, count, offset=None): 150 | """Return the next `count' bytes of input. Raise error on 151 | end-of-input.""" 152 | if offset is None: 153 | offset = self.m_offset 154 | update_offset = True 155 | else: 156 | update_offset = False 157 | bytes = self.m_buffer[offset:offset+count] 158 | if len(bytes) != count: 159 | raise Error, 'Premature end of input.' 160 | if update_offset: 161 | self.m_offset += count 162 | return bytes 163 | 164 | 165 | class Client(object): 166 | """A client for the netlogon service. 167 | 168 | This client can make multiple simultaneous netlogon calls. 169 | """ 170 | 171 | _timeout = 2 172 | _retries = 3 173 | _bufsize = 8192 174 | 175 | def __init__(self): 176 | """Constructor.""" 177 | self.m_socket = None 178 | self.m_queries = {} 179 | self.m_offset = None 180 | 181 | def query(self, addr, domain): 182 | """Add the Netlogon query to `addr' for `domain'.""" 183 | hostname, port = addr 184 | addr = (socket.gethostbyname(hostname), port) 185 | self.m_queries[addr] = [hostname, port, domain, None] 186 | 187 | def call(self, timeout=None, retries=None): 188 | """Wait for results for `timeout' seconds.""" 189 | if timeout is None: 190 | timeout = self._timeout 191 | if retries is None: 192 | retries = self._retries 193 | result = [] 194 | self._create_socket() 195 | for i in range(retries): 196 | if not self.m_queries: 197 | break 198 | self._send_all_requests() 199 | result += self._wait_for_replies(timeout) 200 | self._close_socket() 201 | self.m_queries = {} 202 | return result 203 | 204 | def _create_socket(self): 205 | """Create an UDP socket for `server':`port'.""" 206 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 207 | sock.bind(('', 0)) 208 | self.m_socket = sock 209 | 210 | def _close_socket(self): 211 | """Close the UDP socket.""" 212 | self.m_socket.close() 213 | self.m_socket = None 214 | 215 | def _create_message_id(self): 216 | """Create a new sequence number.""" 217 | if self.m_offset is None: 218 | self.m_offset = random.randint(0, 2**31-1) 219 | msgid = self.m_offset 220 | self.m_offset += 1 221 | if self.m_offset == 2**31-1: 222 | self.m_offset = 0 223 | return msgid 224 | 225 | def _send_all_requests(self): 226 | """Send requests to all hosts.""" 227 | for addr in self.m_queries: 228 | domain = self.m_queries[addr][2] 229 | msgid = self._create_message_id() 230 | self.m_queries[addr][3] = msgid 231 | packet = self._create_netlogon_query(domain, msgid) 232 | self.m_socket.sendto(packet, 0, addr) 233 | 234 | def _wait_for_replies(self, timeout): 235 | """Wait one single timeout on all the sockets.""" 236 | begin = time.time() 237 | end = begin + timeout 238 | replies = [] 239 | while True: 240 | if not self.m_queries: 241 | break 242 | timeleft = end - time.time() 243 | if timeleft <= 0: 244 | break 245 | fds = [ self.m_socket.fileno() ] 246 | try: 247 | result = select.select(fds, [], [], timeleft) 248 | except select.error, err: 249 | error = err.args[0] 250 | if error == errno.EINTR: 251 | continue # interrupted by signal 252 | else: 253 | raise Error, str(err) # unrecoverable 254 | if not result[0]: 255 | continue # timeout 256 | assert fds == result[0] 257 | while True: 258 | if not self.m_queries: 259 | break 260 | try: 261 | data, addr = self.m_socket.recvfrom(self._bufsize, 262 | socket.MSG_DONTWAIT) 263 | except socket.error, err: 264 | error = err.args[0] 265 | if error == errno.EINTR: 266 | continue # signal interrupt 267 | elif error == errno.EAGAIN: 268 | break # no data available now 269 | else: 270 | raise Error, str(err) # unrecoverable 271 | try: 272 | hostname, port, domain, msgid = self.m_queries[addr] 273 | except KeyError: 274 | continue # someone sent us an erroneous datagram? 275 | try: 276 | id, opcode = self._parse_message_header(data) 277 | except (asn1.Error, ldap.Error, Error): 278 | continue 279 | if id != msgid: 280 | continue 281 | del self.m_queries[addr] 282 | try: 283 | reply = self._parse_netlogon_reply(data) 284 | except (asn1.Error, ldap.Error, Error): 285 | continue 286 | if not reply: 287 | continue 288 | reply.q_hostname = hostname 289 | reply.q_port = port 290 | reply.q_domain = domain 291 | reply.q_msgid = msgid 292 | reply.q_address = addr 293 | timing = time.time() - begin 294 | reply.q_timing = timing 295 | replies.append(reply) 296 | return replies 297 | 298 | def _create_netlogon_query(self, domain, msgid): 299 | """Create a netlogon query for `domain'.""" 300 | client = ldap.Client() 301 | hostname = misc.hostname() 302 | filter = '(&(DnsDomain=%s)(Host=%s)(NtVer=\\06\\00\\00\\00))' % \ 303 | (domain, hostname) 304 | attrs = ('NetLogon',) 305 | query = client.create_search_request('', filter, attrs=attrs, 306 | scope=ldap.SCOPE_BASE, msgid=msgid) 307 | return query 308 | 309 | def _parse_message_header(self, reply): 310 | """Parse an LDAP header and return the messageid and opcode.""" 311 | client = ldap.Client() 312 | msgid, opcode = client.parse_message_header(reply) 313 | return msgid, opcode 314 | 315 | def _parse_netlogon_reply(self, reply): 316 | """Parse a netlogon reply.""" 317 | client = ldap.Client() 318 | messages = client.parse_search_result(reply) 319 | if not messages: 320 | return 321 | msgid, dn, attrs = messages[0] 322 | if not attrs.get('netlogon'): 323 | raise Error, 'No netlogon attribute received.' 324 | data = attrs['netlogon'][0] 325 | decoder = Decoder() 326 | decoder.start(data) 327 | result = decoder.parse() 328 | return result 329 | -------------------------------------------------------------------------------- /lib/ad/protocol/krb5.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of FreeADI. FreeADI is free software that is made 3 | * available under the MIT license. Consult the file "LICENSE" that is 4 | * distributed together with this file for the exact licensing terms. 5 | * 6 | * FreeADI is copyright (c) 2007 by the FreeADI authors. See the file 7 | * "AUTHORS" for a complete overview. 8 | */ 9 | 10 | #include 11 | #include 12 | 13 | 14 | static PyObject *k5_error; 15 | 16 | #define RETURN_ON_ERROR(message, code) \ 17 | do if (code != 0) \ 18 | { \ 19 | const char *error; \ 20 | error = krb5_get_error_message(ctx, code); \ 21 | PyErr_Format(k5_error, "%s: %s", message, error); \ 22 | krb5_free_error_message(ctx, error); \ 23 | return NULL; \ 24 | } while (0) 25 | 26 | 27 | static PyObject * 28 | k5_get_init_creds_password(PyObject *self, PyObject *args) 29 | { 30 | char *name, *password; 31 | krb5_context ctx; 32 | krb5_error_code code; 33 | krb5_ccache ccache; 34 | krb5_principal principal; 35 | krb5_get_init_creds_opt options; 36 | krb5_creds creds; 37 | 38 | if (!PyArg_ParseTuple(args, "ss", &name, &password)) 39 | return NULL; 40 | 41 | /* Initialize parameters. */ 42 | code = krb5_init_context(&ctx); 43 | RETURN_ON_ERROR("krb5_init_context()", code); 44 | code = krb5_parse_name(ctx, name, &principal); 45 | RETURN_ON_ERROR("krb5_parse_name()", code); 46 | krb5_get_init_creds_opt_init(&options); 47 | memset(&creds, 0, sizeof (creds)); 48 | 49 | /* Get the credentials. */ 50 | code = krb5_get_init_creds_password(ctx, &creds, principal, password, 51 | NULL, NULL, 0, NULL, &options); 52 | RETURN_ON_ERROR("krb5_get_init_creds_password()", code); 53 | 54 | /* Store the credential in the credential cache. */ 55 | code = krb5_cc_default(ctx, &ccache); 56 | RETURN_ON_ERROR("krb5_cc_default()", code); 57 | code = krb5_cc_initialize(ctx, ccache, principal); 58 | RETURN_ON_ERROR("krb5_cc_initialize()", code); 59 | code = krb5_cc_store_cred(ctx, ccache, &creds); 60 | RETURN_ON_ERROR("krb5_cc_store_creds()", code); 61 | krb5_cc_close(ctx, ccache); 62 | 63 | Py_INCREF(Py_None); 64 | return Py_None; 65 | } 66 | 67 | 68 | static PyObject * 69 | k5_get_init_creds_keytab(PyObject *self, PyObject *args) 70 | { 71 | char *name, *ktname; 72 | krb5_context ctx; 73 | krb5_error_code code; 74 | krb5_keytab keytab; 75 | krb5_ccache ccache; 76 | krb5_principal principal; 77 | krb5_get_init_creds_opt options; 78 | krb5_creds creds; 79 | 80 | if (!PyArg_ParseTuple(args, "sz", &name, &ktname)) 81 | return NULL; 82 | 83 | /* Initialize parameters. */ 84 | code = krb5_init_context(&ctx); 85 | RETURN_ON_ERROR("krb5_init_context()", code); 86 | code = krb5_parse_name(ctx, name, &principal); 87 | RETURN_ON_ERROR("krb5_parse_name()", code); 88 | krb5_get_init_creds_opt_init(&options); 89 | memset(&creds, 0, sizeof (creds)); 90 | 91 | /* Resolve keytab */ 92 | if (ktname) 93 | { 94 | code = krb5_kt_resolve(ctx, ktname, &keytab); 95 | RETURN_ON_ERROR("krb5_kt_resolve()", code); 96 | } else 97 | { 98 | code = krb5_kt_default(ctx, &keytab); 99 | RETURN_ON_ERROR("krb5_kt_resolve()", code); 100 | } 101 | 102 | /* Get the credentials. */ 103 | code = krb5_get_init_creds_keytab(ctx, &creds, principal, 104 | keytab, 0, NULL, &options); 105 | RETURN_ON_ERROR("krb5_get_init_creds_keytab()", code); 106 | 107 | /* Store the credential in the credential cache. */ 108 | code = krb5_cc_default(ctx, &ccache); 109 | RETURN_ON_ERROR("krb5_cc_default()", code); 110 | code = krb5_cc_initialize(ctx, ccache, principal); 111 | RETURN_ON_ERROR("krb5_cc_initialize()", code); 112 | code = krb5_cc_store_cred(ctx, ccache, &creds); 113 | RETURN_ON_ERROR("krb5_cc_store_creds()", code); 114 | krb5_cc_close(ctx, ccache); 115 | 116 | Py_INCREF(Py_None); 117 | return Py_None; 118 | } 119 | 120 | 121 | static void 122 | _k5_set_password_error(krb5_data *result_code_string, krb5_data *result_string) 123 | { 124 | char *p1, *p2; 125 | 126 | p1 = malloc(result_code_string->length+1); 127 | if (p1 == NULL) 128 | { 129 | PyErr_NoMemory(); 130 | return; 131 | } 132 | if (result_code_string->data) 133 | { 134 | strncpy(p1, result_code_string->data, result_code_string->length); 135 | } 136 | p1[result_code_string->length] = '\000'; 137 | 138 | p2 = malloc(result_string->length+1); 139 | if (p1 == NULL) 140 | { 141 | PyErr_NoMemory(); 142 | return; 143 | } 144 | if (result_string->data) 145 | { 146 | strncpy(p1, result_string->data, result_string->length); 147 | } 148 | p2[result_string->length] = '\000'; 149 | 150 | PyErr_Format(k5_error, "%s%s%s", p1, (*p1 && *p2) ? ": " : "", p2); 151 | 152 | free(p1); 153 | free(p2); 154 | } 155 | 156 | 157 | static PyObject * 158 | k5_set_password(PyObject *self, PyObject *args) 159 | { 160 | int result_code; 161 | char *name, *newpass; 162 | krb5_context ctx; 163 | krb5_error_code code; 164 | krb5_principal principal; 165 | krb5_data result_code_string, result_string; 166 | krb5_ccache ccache; 167 | 168 | if (!PyArg_ParseTuple(args, "ss", &name, &newpass)) 169 | return NULL; 170 | 171 | /* Initialize parameters. */ 172 | code = krb5_init_context(&ctx); 173 | RETURN_ON_ERROR("krb5_init_context()", code); 174 | code = krb5_parse_name(ctx, name, &principal); 175 | RETURN_ON_ERROR("krb5_parse_name()", code); 176 | 177 | /* Get credentials */ 178 | code = krb5_cc_default(ctx, &ccache); 179 | RETURN_ON_ERROR("krb5_cc_default()", code); 180 | 181 | /* Set password */ 182 | code = krb5_set_password_using_ccache(ctx, ccache, newpass, principal, 183 | &result_code, &result_code_string, 184 | &result_string); 185 | RETURN_ON_ERROR("krb5_set_password_using_ccache()", code); 186 | 187 | /* Any other error? */ 188 | if (result_code != 0) 189 | { 190 | _k5_set_password_error(&result_code_string, &result_string); 191 | return NULL; 192 | } 193 | 194 | /* Free up results. */ 195 | if (result_code_string.data != NULL) 196 | free(result_code_string.data); 197 | if (result_string.data != NULL) 198 | free(result_string.data); 199 | 200 | Py_INCREF(Py_None); 201 | return Py_None; 202 | } 203 | 204 | 205 | static PyObject * 206 | k5_change_password(PyObject *self, PyObject *args) 207 | { 208 | int result_code; 209 | char *name, *oldpass, *newpass; 210 | krb5_context ctx; 211 | krb5_error_code code; 212 | krb5_principal principal; 213 | krb5_get_init_creds_opt options; 214 | krb5_creds creds; 215 | krb5_data result_code_string, result_string; 216 | 217 | if (!PyArg_ParseTuple(args, "sss", &name, &oldpass, &newpass)) 218 | return NULL; 219 | 220 | /* Initialize parameters. */ 221 | code = krb5_init_context(&ctx); 222 | RETURN_ON_ERROR("krb5_init_context()", code); 223 | code = krb5_parse_name(ctx, name, &principal); 224 | RETURN_ON_ERROR("krb5_parse_name()", code); 225 | 226 | /* Get credentials using the password. */ 227 | krb5_get_init_creds_opt_init(&options); 228 | krb5_get_init_creds_opt_set_tkt_life(&options, 5*60); 229 | krb5_get_init_creds_opt_set_renew_life(&options, 0); 230 | krb5_get_init_creds_opt_set_forwardable(&options, 0); 231 | krb5_get_init_creds_opt_set_proxiable(&options, 0); 232 | memset(&creds, 0, sizeof (creds)); 233 | code = krb5_get_init_creds_password(ctx, &creds, principal, oldpass, 234 | NULL, NULL, 0, "kadmin/changepw", 235 | &options); 236 | RETURN_ON_ERROR("krb5_get_init_creds_password()", code); 237 | 238 | code = krb5_change_password(ctx, &creds, newpass, &result_code, 239 | &result_code_string, &result_string); 240 | RETURN_ON_ERROR("krb5_change_password()", code); 241 | 242 | /* Any other error? */ 243 | if (result_code != 0) 244 | { 245 | _k5_set_password_error(&result_code_string, &result_string); 246 | return NULL; 247 | } 248 | 249 | /* Free up results. */ 250 | if (result_code_string.data != NULL) 251 | free(result_code_string.data); 252 | if (result_string.data != NULL) 253 | free(result_string.data); 254 | 255 | Py_INCREF(Py_None); 256 | return Py_None; 257 | } 258 | 259 | 260 | static PyObject * 261 | k5_cc_default(PyObject *self, PyObject *args) 262 | { 263 | krb5_context ctx; 264 | krb5_error_code code; 265 | krb5_ccache ccache; 266 | const char *name; 267 | PyObject *ret; 268 | 269 | code = krb5_init_context(&ctx); 270 | RETURN_ON_ERROR("krb5_init_context()", code); 271 | code = krb5_cc_default(ctx, &ccache); 272 | RETURN_ON_ERROR("krb5_cc_default()", code); 273 | name = krb5_cc_get_name(ctx, ccache); 274 | if (name == NULL) 275 | { 276 | PyErr_Format(k5_error, "krb5_cc_default() returned NULL"); 277 | return NULL; 278 | } 279 | 280 | ret = PyString_FromString(name); 281 | if (ret == NULL) 282 | return ret; 283 | 284 | code = krb5_cc_close(ctx, ccache); 285 | RETURN_ON_ERROR("krb5_cc_close()", code); 286 | krb5_free_context(ctx); 287 | 288 | return ret; 289 | } 290 | 291 | static PyObject * 292 | k5_cc_copy_creds(PyObject *self, PyObject *args) 293 | { 294 | krb5_context ctx; 295 | char *namein, *nameout; 296 | krb5_error_code code; 297 | krb5_ccache ccin, ccout; 298 | krb5_principal principal; 299 | 300 | if (!PyArg_ParseTuple( args, "ss", &namein, &nameout)) 301 | return NULL; 302 | 303 | code = krb5_init_context(&ctx); 304 | RETURN_ON_ERROR("krb5_init_context()", code); 305 | code = krb5_cc_resolve(ctx, namein, &ccin); 306 | RETURN_ON_ERROR("krb5_cc_resolve()", code); 307 | code = krb5_cc_get_principal(ctx, ccin, &principal); 308 | RETURN_ON_ERROR("krb5_cc_get_principal()", code); 309 | 310 | code = krb5_cc_resolve(ctx, nameout, &ccout); 311 | RETURN_ON_ERROR("krb5_cc_resolve()", code); 312 | code = krb5_cc_initialize(ctx, ccout, principal); 313 | RETURN_ON_ERROR("krb5_cc_get_initialize()", code); 314 | code = krb5_cc_copy_creds(ctx, ccin, ccout); 315 | RETURN_ON_ERROR("krb5_cc_copy_creds()", code); 316 | 317 | code = krb5_cc_close(ctx, ccin); 318 | RETURN_ON_ERROR("krb5_cc_close()", code); 319 | code = krb5_cc_close(ctx, ccout); 320 | RETURN_ON_ERROR("krb5_cc_close()", code); 321 | krb5_free_principal(ctx, principal); 322 | krb5_free_context(ctx); 323 | 324 | Py_INCREF(Py_None); 325 | return Py_None; 326 | } 327 | 328 | 329 | static PyObject * 330 | k5_cc_get_principal(PyObject *self, PyObject *args) 331 | { 332 | krb5_context ctx; 333 | char *ccname, *name; 334 | krb5_error_code code; 335 | krb5_ccache ccache; 336 | krb5_principal principal; 337 | PyObject *ret; 338 | 339 | if (!PyArg_ParseTuple( args, "s", &ccname)) 340 | return NULL; 341 | 342 | code = krb5_init_context(&ctx); 343 | RETURN_ON_ERROR("krb5_init_context()", code); 344 | code = krb5_cc_resolve(ctx, ccname, &ccache); 345 | RETURN_ON_ERROR("krb5_cc_resolve()", code); 346 | code = krb5_cc_get_principal(ctx, ccache, &principal); 347 | RETURN_ON_ERROR("krb5_cc_get_principal()", code); 348 | code = krb5_unparse_name(ctx, principal, &name); 349 | RETURN_ON_ERROR("krb5_unparse_name()", code); 350 | 351 | ret = PyString_FromString(name); 352 | if (ret == NULL) 353 | return ret; 354 | 355 | code = krb5_cc_close(ctx, ccache); 356 | RETURN_ON_ERROR("krb5_cc_close()", code); 357 | krb5_free_unparsed_name(ctx, name); 358 | krb5_free_principal(ctx, principal); 359 | krb5_free_context(ctx); 360 | 361 | return ret; 362 | } 363 | 364 | 365 | static PyObject * 366 | k5_c_valid_enctype(PyObject *self, PyObject *args) 367 | { 368 | char *name; 369 | krb5_context ctx; 370 | krb5_enctype type; 371 | krb5_error_code code; 372 | krb5_boolean valid; 373 | PyObject *ret; 374 | 375 | if (!PyArg_ParseTuple( args, "s", &name)) 376 | return NULL; 377 | 378 | code = krb5_init_context(&ctx); 379 | RETURN_ON_ERROR("krb5_init_context()", code); 380 | code = krb5_string_to_enctype(name, &type); 381 | RETURN_ON_ERROR("krb5_string_to_enctype()", code); 382 | valid = krb5_c_valid_enctype(type); 383 | ret = PyBool_FromLong((long) valid); 384 | krb5_free_context(ctx); 385 | 386 | return ret; 387 | } 388 | 389 | 390 | static PyMethodDef k5_methods[] = 391 | { 392 | { "get_init_creds_password", 393 | (PyCFunction) k5_get_init_creds_password, METH_VARARGS }, 394 | { "get_init_creds_keytab", 395 | (PyCFunction) k5_get_init_creds_keytab, METH_VARARGS }, 396 | { "set_password", 397 | (PyCFunction) k5_set_password, METH_VARARGS }, 398 | { "change_password", 399 | (PyCFunction) k5_change_password, METH_VARARGS }, 400 | { "cc_default", 401 | (PyCFunction) k5_cc_default, METH_VARARGS }, 402 | { "cc_copy_creds", 403 | (PyCFunction) k5_cc_copy_creds, METH_VARARGS }, 404 | { "cc_get_principal", 405 | (PyCFunction) k5_cc_get_principal, METH_VARARGS }, 406 | { "c_valid_enctype", 407 | (PyCFunction) k5_c_valid_enctype, METH_VARARGS }, 408 | { NULL, NULL } 409 | }; 410 | 411 | 412 | void 413 | initkrb5(void) 414 | { 415 | PyObject *module, *dict; 416 | 417 | initialize_krb5_error_table(); 418 | 419 | module = Py_InitModule("krb5", k5_methods); 420 | dict = PyModule_GetDict(module); 421 | k5_error = PyErr_NewException("freeadi.protocol.krb5.Error", NULL, NULL); 422 | PyDict_SetItemString(dict, "Error", k5_error); 423 | } 424 | -------------------------------------------------------------------------------- /lib/ad/core/creds.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import os 10 | import time 11 | import logging 12 | import tempfile 13 | import ldap 14 | 15 | from ad.core.object import factory 16 | from ad.core.exception import Error 17 | from ad.core.locate import Locator 18 | from ad.core.locate import KERBEROS_PORT, KPASSWD_PORT 19 | from ad.protocol import krb5 20 | from ad.util import compat 21 | 22 | 23 | class Creds(object): 24 | """AD credential management.""" 25 | 26 | c_config_stack = {} 27 | c_ccache_stack = {} 28 | 29 | # We only use strong encryption mechanisms so no DES here. RC4 has been 30 | # available since the first AD version in Windows 2000 so there's no 31 | # backward compatibility issue. AES has been available since Windows 2008. 32 | # In case of older Windows version it is not a problem that we include AES 33 | # as Kerberos will correctly detect what encryption type to use. 34 | # 35 | # Note: there's an interoperability issue for computer accounts in Windows 36 | # 2003 and later. In Windows 2003, AD changed its principal2salt() 37 | # function for computer accounts which means it no longer matches the 38 | # implementation in MIT Kerberos. One consequence of this is that keytabs 39 | # created by "ktutil" on Unix will be different from keytabs created by 40 | # "ktpass" on Windows. Keytabs created for computer accounts on Windows 41 | # 2000 still work correctly, and so do keytabs for regular user accounts 42 | # on any Windows version. 43 | # 44 | # If you're authenticating computer accounts with keytabs, there's two 45 | # options. The first is to use the "arcfour-hmac-md5" encryption type 46 | # which is unsalted. The second option is to use "msktutil" to create the 47 | # keytab on the Unix side. 48 | c_enctypes = ('aes256-cts-hmac-sha1-96', 49 | 'aes128-cts-hmac-sha1-96', 50 | 'arcfour-hmac-md5') 51 | c_supported_enctypes = None 52 | 53 | def __init__(self, domain, use_system_config=False): 54 | """Constructor. 55 | 56 | The `domain' parameter specifies the default domain. The default 57 | domain is used when a principal is specified without a domain in the 58 | .acquire() method. 59 | """ 60 | self.m_domain = domain.upper() 61 | self.m_domains = {} 62 | self.m_principal = None 63 | self.m_ccache = None 64 | self.m_config = None 65 | self.m_use_system_config = use_system_config 66 | self.m_config_cleanup = [] 67 | self.m_logger = logging.getLogger('ad.core.creds') 68 | 69 | def __del__(self): 70 | """Destructor. This releases all currently held credentials and cleans 71 | up temporary files.""" 72 | self.release() 73 | 74 | def load(self): 75 | """Load credentials from the OS.""" 76 | ccache = krb5.cc_default() 77 | if not os.access(ccache, os.R_OK): 78 | raise Error, 'No ccache found' 79 | self.m_principal = krb5.cc_get_principal(ccache) 80 | self._init_ccache() 81 | krb5.cc_copy_creds(ccache, self.m_ccache) 82 | self._activate_ccache() 83 | self._resolve_servers_for_domain(self.m_domain) 84 | 85 | def acquire(self, principal, password=None, keytab=None, server=None): 86 | """Acquire credentials for `principal'. 87 | 88 | The `principal' argument specifies the principal for which to acquire 89 | credentials. If it is not a qualified pricipal in the form of 90 | principal@domain, then the default domain is assumed. 91 | 92 | If `password' is given, it is the password to the principal. 93 | Otherwise, `keytab' specifies the keytab to use. The `keytab' argument 94 | can either specify the absolute path name of a file, or can be None 95 | (the default) which uses the system-specific default keytab. 96 | """ 97 | if '@' in principal: 98 | principal, domain = principal.split('@') 99 | domain = domain.upper() 100 | else: 101 | domain = self.m_domain 102 | principal = '%s@%s' % (principal, domain) 103 | self._init_ccache() 104 | self._activate_ccache() 105 | if not self.m_use_system_config: 106 | if server is None: 107 | self._resolve_servers_for_domain(domain) 108 | else: 109 | self._set_servers_for_domain(domain, [server]) 110 | try: 111 | if password is not None: 112 | krb5.get_init_creds_password(principal, password) 113 | else: 114 | krb5.get_init_creds_keytab(principal, keytab) 115 | except krb5.Error, err: 116 | raise Error, str(err) 117 | self.m_principal = principal 118 | 119 | def release(self): 120 | """Release all credentials.""" 121 | self._release_ccache() 122 | self._release_config() 123 | self.m_principal = None 124 | 125 | def principal(self): 126 | """Return the current principal.""" 127 | return self.m_principal 128 | 129 | def _ccache_name(self): 130 | """Return the Kerberos credential cache file name.""" 131 | return self.m_ccache 132 | 133 | def _config_name(self): 134 | """Return the Kerberos config file name.""" 135 | return self.m_config 136 | 137 | def _init_ccache(self): 138 | """Initialize Kerberos ccache.""" 139 | if self.m_ccache: 140 | return 141 | fd, fname = tempfile.mkstemp() 142 | os.close(fd) 143 | self.m_ccache = fname 144 | 145 | def _activate_ccache(self): 146 | """Active our private credential cache.""" 147 | assert self.m_ccache is not None 148 | orig = self._environ('KRB5CCNAME') 149 | if orig != self.m_ccache: 150 | self._set_environ('KRB5CCNAME', self.m_ccache) 151 | self.c_ccache_stack[self.m_ccache] = (True, orig) 152 | 153 | def _release_ccache(self): 154 | """Release the current Kerberos configuration.""" 155 | if not self.m_ccache: 156 | return 157 | # Things are complicated by the fact that multiple instances of this 158 | # class my exist. Therefore we need to keep track whether we have set 159 | # the current $KRB5CCNAME or someone else. If it is ourselves we are 160 | # fine, but if not we need to mark that the class who replaced our 161 | # value should not point back to us when that class releases its 162 | # credentials because we are releasing those credentials now. 163 | assert self.m_ccache in self.c_ccache_stack 164 | active, orig = self.c_ccache_stack[self.m_ccache] 165 | assert active 166 | ccache = self._environ('KRB5CCNAME') 167 | if ccache == self.m_ccache: 168 | while True: 169 | active, orig = self.c_ccache_stack[ccache] 170 | del self.c_ccache_stack[ccache] 171 | if orig not in self.c_ccache_stack or \ 172 | self.c_ccache_stack[orig][0]: 173 | self._set_environ('KRB5CCNAME', orig) 174 | break 175 | ccache = orig 176 | else: 177 | self.c_ccache_stack[self.m_ccache] = (False, orig) 178 | try: 179 | os.remove(self.m_ccache) 180 | except OSError: 181 | pass 182 | self.m_ccache = None 183 | 184 | def _resolve_servers_for_domain(self, domain, force=False): 185 | """Resolve domain controllers for a domain.""" 186 | if self.m_use_system_config: 187 | return 188 | if domain in self.m_domains and not force: 189 | return 190 | locator = factory(Locator) 191 | result = locator.locate_many(domain) 192 | if not result: 193 | m = 'No suitable domain controllers found for %s' % domain 194 | raise Error, m 195 | self.m_domains[domain] = list(result) 196 | # Re-init every time 197 | self._init_config() 198 | self._write_config() 199 | self._activate_config() 200 | 201 | def _set_servers_for_domain(self, domain, servers): 202 | """Set the servers to use for `domain'.""" 203 | if self.m_use_system_config: 204 | return 205 | self.m_domains[domain] = servers 206 | self._init_config() 207 | self._write_config() 208 | self._activate_config() 209 | 210 | def _init_config(self): 211 | """Initialize Kerberos config.""" 212 | if self.m_config and self.m_config in self.c_config_stack: 213 | # Delete current config and create one under a new file name. This 214 | # seems to be required by the Kerberos libraries that do not 215 | # reload the configuration file otherwise. 216 | active, orig = self.c_config_stack[self.m_config] 217 | self.c_config_stack[self.m_config] = (False, orig) 218 | # unlink this config file on activate of new config 219 | self.m_config_cleanup.append(self.m_config) 220 | fd, fname = tempfile.mkstemp() 221 | os.close(fd) 222 | self.m_config = fname 223 | 224 | def _supported_enctypes(self): 225 | """Return a string of supported encryption types.""" 226 | if self.c_supported_enctypes is not None: 227 | return self.c_supported_enctypes 228 | result = [] 229 | for enctype in self.c_enctypes: 230 | if krb5.c_valid_enctype(enctype): 231 | result.append(enctype) 232 | self.c_supported_enctypes = result 233 | self.m_logger.info('using encryption types: %s' % ' '.join(result)) 234 | return result 235 | 236 | def _write_config(self): 237 | """Write the Kerberos configuration file.""" 238 | assert self.m_config is not None 239 | ftmp = '%s.%d-tmp' % (self.m_config, os.getpid()) 240 | fout = file(ftmp, 'w') 241 | enctypes = ' '.join(self._supported_enctypes()) 242 | try: 243 | fout.write('# krb5.conf generated by Python-AD at %s\n' % 244 | time.asctime()) 245 | fout.write('[libdefaults]\n') 246 | fout.write(' default_realm = %s\n' % self.m_domain) 247 | fout.write(' dns_lookup_kdc = false\n') 248 | fout.write(' default_tgs_enctypes = %s\n' % enctypes) 249 | fout.write(' default_tkt_enctypes = %s\n' % enctypes) 250 | if compat.disable_reverse_dns(): 251 | fout.write(' rdns = no\n') 252 | fout.write('[realms]\n') 253 | for domain in self.m_domains: 254 | fout.write(' %s = {\n' % domain) 255 | for server in self.m_domains[domain]: 256 | fout.write(' kdc = %s:%d\n' % (server, KERBEROS_PORT)) 257 | fout.write(' kpasswd_server = %s:%d\n' 258 | % (server, KPASSWD_PORT)) 259 | fout.write(' }\n') 260 | fout.close() 261 | os.rename(ftmp, self.m_config) 262 | finally: 263 | try: 264 | os.remove(ftmp) 265 | except OSError: 266 | pass 267 | 268 | def _activate_config(self): 269 | """Activate the Kerberos config.""" 270 | if self.m_use_system_config: 271 | return 272 | assert self.m_config is not None 273 | orig = self._environ('KRB5_CONFIG') 274 | if orig != self.m_config: 275 | self._set_environ('KRB5_CONFIG', self.m_config) 276 | self.c_config_stack[self.m_config] = (True, orig) 277 | for fname in self.m_config_cleanup: 278 | try: 279 | os.remove(fname) 280 | except OSError: 281 | pass 282 | self.m_config_cleanup = [] 283 | 284 | def _release_config(self): 285 | """Release the current Kerberos configuration.""" 286 | if self.m_use_system_config or not self.m_config: 287 | return 288 | # See the comments with _release_ccache(). 289 | assert self.m_config in self.c_config_stack 290 | active, orig = self.c_config_stack[self.m_config] 291 | assert active 292 | config = self._environ('KRB5_CONFIG') 293 | if config == self.m_config: 294 | while True: 295 | active, orig = self.c_config_stack[config] 296 | del self.c_config_stack[config] 297 | if orig not in self.c_config_stack or \ 298 | self.c_config_stack[orig][0]: 299 | self._set_environ('KRB5_CONFIG', orig) 300 | break 301 | config = orig 302 | else: 303 | self.c_config_stack[self.m_config] = (False, orig) 304 | try: 305 | os.remove(self.m_config) 306 | except OSError: 307 | pass 308 | self.m_config = None 309 | 310 | def _environ(self, name): 311 | """Return an environment variable or None in case it doesn't exist.""" 312 | return os.environ.get(name) 313 | 314 | def _set_environ(self, name, value): 315 | """Set or delete an environment variable.""" 316 | if value is None: 317 | try: 318 | del os.environ[name] 319 | except KeyError: 320 | pass 321 | else: 322 | os.environ[name] = value 323 | -------------------------------------------------------------------------------- /lib/ad/core/locate.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | import time 10 | import random 11 | import logging 12 | 13 | import ldap 14 | import dns.resolver 15 | import dns.reversename 16 | import dns.exception 17 | 18 | from ad.protocol import netlogon 19 | from ad.protocol.netlogon import Client as NetlogonClient 20 | from ad.core.exception import Error as ADError 21 | from ad.util import compat 22 | 23 | 24 | LDAP_PORT = 389 25 | KERBEROS_PORT = 88 26 | KPASSWD_PORT = 464 27 | 28 | 29 | class Locator(object): 30 | """Locate domain controllers. 31 | 32 | The function of is class is to locate, select, and order domain 33 | controllers for a given domain. 34 | 35 | The default selection mechanism discards domain controllers that do not 36 | have a proper reverse DNS name set. These domain controllers are not 37 | usable with SASL/GSSAPI that uses hostname canonicalisation based on 38 | reverse DNS. 39 | 40 | The default ordering mechanism has two different policies: 41 | 42 | - For local domain controllers we order on the priority and weight that 43 | has been configured for the SRV records. 44 | - For remote domain controllers we order on timing only and we ignore 45 | priority and weight. This may or may not be what you want. From my 46 | experience, priorities and weights are often not set up at all. In this 47 | situation is is preferable to use timing information to order domain 48 | controllers. 49 | 50 | Both policies (selection and ordering) can be changed by subclassing this 51 | class. 52 | """ 53 | 54 | _maxservers = 3 55 | _timeout = 300 # cache entries for 5 minutes 56 | 57 | def __init__(self, site=None): 58 | """Constructor.""" 59 | self.m_site = site 60 | self.m_site_detected = False 61 | self.m_logger = logging.getLogger('ad.core.locate') 62 | self.m_cache = {} 63 | self.m_timeout = self._timeout 64 | 65 | def locate(self, domain, role=None): 66 | """Locate one domain controller.""" 67 | servers = self.locate_many(domain, role, maxservers=1) 68 | if not servers: 69 | m = 'Could not locate domain controller' 70 | raise ADError, m 71 | return servers[0] 72 | 73 | def locate_many(self, domain, role=None, maxservers=None): 74 | """Locate a list of up to `maxservers' of domain controllers.""" 75 | result = self.locate_many_ex(domain, role, maxservers) 76 | result = [ r.hostname for r in result ] 77 | return result 78 | 79 | def locate_many_ex(self, domain, role=None, maxservers=None): 80 | """Like locate_many(), but returns a list of netlogon.Reply objects 81 | instead.""" 82 | if role is None: 83 | role = 'dc' 84 | if maxservers is None: 85 | maxservers = self._maxservers 86 | if role not in ('dc', 'gc', 'pdc'): 87 | raise ValueError, 'Role should be one of "dc", "gc" or "pdc".' 88 | if role == 'pdc': 89 | maxservers = 1 90 | domain = domain.upper() 91 | self.m_logger.debug('locating domain controllers for %s (role %s)' % 92 | (domain, role)) 93 | key = (domain, role) 94 | if key in self.m_cache: 95 | stamp, nrequested, servers = self.m_cache[key] 96 | now = time.time() 97 | if now - stamp < self._timeout and nrequested >= maxservers: 98 | self.m_logger.debug('domain controllers found in cache') 99 | return servers 100 | self.m_logger.debug('domain controllers not in cache, going to network') 101 | servers = [] 102 | candidates = [] 103 | if self.m_site is None and not self.m_site_detected: 104 | self.m_site = self._detect_site(domain) 105 | self.m_site_detected = True 106 | if self.m_site and role != 'pdc': 107 | query = '_ldap._tcp.%s._sites.%s._msdcs.%s' % \ 108 | (self.m_site, role, domain.lower()) 109 | answer = self._dns_query(query, 'SRV') 110 | candidates += self._order_dns_srv(answer) 111 | query = '_ldap._tcp.%s._msdcs.%s' % (role, domain.lower()) 112 | answer = self._dns_query(query, 'SRV') 113 | candidates += self._order_dns_srv(answer) 114 | addresses = self._extract_addresses_from_srv(candidates) 115 | addresses = self._remove_duplicates(addresses) 116 | replies = [] 117 | netlogon = NetlogonClient() 118 | for i in range(0, len(addresses), maxservers): 119 | for addr in addresses[i:i+maxservers]: 120 | addr = (addr[0], LDAP_PORT) # in case we queried for GC 121 | netlogon.query(addr, domain) 122 | replies += netlogon.call() 123 | if self._sufficient_domain_controllers(replies, role, maxservers): 124 | break 125 | servers = self._select_domain_controllers(replies, role, maxservers, 126 | addresses) 127 | self.m_logger.debug('found %d domain controllers' % len(servers)) 128 | now = time.time() 129 | self.m_cache[key] = (now, maxservers, servers) 130 | return servers 131 | 132 | def check_domain_controller(self, server, domain, role): 133 | """Ensure that `server' is a domain controller for `domain' and has 134 | role `role'. 135 | """ 136 | addr = (server, LDAP_PORT) 137 | client = NetlogonClient() 138 | client.query(addr, domain.upper()) 139 | result = client.call() 140 | if len(result) != 1: 141 | return False 142 | reply = result[0] 143 | result = self._check_domain_controller(reply, role) 144 | return result 145 | 146 | def _dns_query(self, query, type): 147 | """Perform a DNS query.""" 148 | self.m_logger.debug('DNS query %s type %s' % (query, type)) 149 | try: 150 | answer = dns.resolver.query(query, type) 151 | except dns.exception.DNSException, err: 152 | answer = [] 153 | self.m_logger.error('DNS query error: %s' % (str(err) or err.__doc__)) 154 | else: 155 | self.m_logger.debug('DNS query returned %d results' % len(answer)) 156 | return answer 157 | 158 | def _detect_site(self, domain): 159 | """Detect our site using the netlogon protocol.""" 160 | self.m_logger.debug('detecting site') 161 | query = '_ldap._tcp.%s' % domain.lower() 162 | answer = self._dns_query(query, 'SRV') 163 | servers = self._order_dns_srv(answer) 164 | addresses = self._extract_addresses_from_srv(servers) 165 | replies = [] 166 | netlogon = NetlogonClient() 167 | for i in range(0, len(addresses), 3): 168 | for addr in addresses[i:i+3]: 169 | self.m_logger.debug('NetLogon query to %s' % addr[0]) 170 | netlogon.query(addr, domain) 171 | replies += netlogon.call() 172 | self.m_logger.debug('%d replies' % len(replies)) 173 | if replies >= 3: 174 | break 175 | if not replies: 176 | self.m_logger.error('could not detect site') 177 | return 178 | sites = {} 179 | for reply in replies: 180 | try: 181 | sites[reply.client_site] += 1 182 | except KeyError: 183 | sites[reply.client_site] = 1 184 | sites = [ (value, key) for key,value in sites.items() ] 185 | sites.sort() 186 | self.m_logger.debug('site detected as %s' % sites[-1][1]) 187 | return sites[0][1] 188 | 189 | def _order_dns_srv(self, answer): 190 | """Order the results of a DNS SRV query.""" 191 | answer = list(answer) 192 | answer.sort(lambda x,y: x.priority - y.priority) 193 | result = [] 194 | for i in range(len(answer)): 195 | if i == 0: 196 | low = i 197 | prio = answer[i].priority 198 | if i > 0 and answer[i].priority != prio: 199 | result += self._srv_weighted_shuffle(answer[low:i]) 200 | low = i 201 | prio = answer[i].priority 202 | elif i == len(answer)-1: 203 | result += self._srv_weighted_shuffle(answer[low:]) 204 | return result 205 | 206 | def _srv_weighted_shuffle(self, answer): 207 | """Do a weighted shuffle on the SRV query result `result'.""" 208 | result = [] 209 | for i in range(len(answer)): 210 | total = 0 211 | cumulative = [] 212 | for j in range(len(answer)): 213 | total += answer[j].weight 214 | cumulative.append((total, j)) 215 | rnd = random.randrange(0, total) 216 | for j in range(len(answer)): 217 | if rnd < cumulative[j][0]: 218 | k = cumulative[j][1] 219 | result.append(answer[k]) 220 | del answer[k] 221 | break 222 | return result 223 | 224 | def _extract_addresses_from_srv(self, answer): 225 | """Extract IP addresses from a DNS SRV query answer.""" 226 | result = [ (a.target.to_text(), a.port) for a in answer ] 227 | return result 228 | 229 | def _remove_duplicates(self, servers): 230 | """Remove duplicates for `servers', keeping the order.""" 231 | dict = {} 232 | result = [] 233 | for srv in servers: 234 | if srv not in dict: 235 | result.append(srv) 236 | dict[srv] = True 237 | return result 238 | 239 | def _check_domain_controller(self, reply, role): 240 | """Check that `server' is a domain controller for `domain' and has 241 | role `role'. 242 | """ 243 | self.m_logger.debug('Checking controller %s for domain %s role %s' % 244 | (reply.q_hostname, reply.q_domain, role)) 245 | answer = self._dns_query(reply.q_hostname, 'A') 246 | if len(answer) != 1: 247 | self.m_logger.error('Forward DNS returned %d entries (need 1)' % 248 | len(answer)) 249 | return False 250 | address = answer[0].address 251 | if not compat.disable_reverse_dns(): 252 | revname = dns.reversename.from_address(address) 253 | answer = self._dns_query(revname, 'PTR') 254 | if len(answer) != 1: 255 | self.m_logger.error('Reverse DNS returned %d entries (need 1)' 256 | % len(answer)) 257 | return False 258 | hostname = answer[0].target.to_text() 259 | answer = self._dns_query(hostname, 'A') 260 | if len(answer) != 1: 261 | self.m_logger.error('Second fwd DNS returned %d entries (need 1)' 262 | % len(answer)) 263 | return False 264 | if answer[0].address != address: 265 | self.m_logger.error('Second forward DNS does not match first') 266 | return False 267 | if role == 'gc' and not (reply.flags & netlogon.SERVER_GC) or \ 268 | role == 'pdc' and not (reply.flags & netlogon.SERVER_PDC) or \ 269 | role == 'dc' and not (reply.flags & netlogon.SERVER_LDAP): 270 | self.m_logger.error('Role does not match') 271 | return False 272 | if reply.q_domain.lower() != reply.domain.lower(): 273 | self.m_logger.error('Domain does not match') 274 | return False 275 | self.m_logger.debug('Controller is OK') 276 | return True 277 | 278 | def _sufficient_domain_controllers(self, replies, role, maxservers): 279 | """Return True if there are sufficient domain controllers in `replies' 280 | to satisfy `maxservers'.""" 281 | total = 0 282 | for reply in replies: 283 | if not hasattr(reply, 'checked'): 284 | checked = self._check_domain_controller(reply, role) 285 | reply.checked = checked 286 | if reply.checked: 287 | total += 1 288 | return total >= maxservers 289 | 290 | def _select_domain_controllers(self, replies, role, maxservers, addresses): 291 | """Select up to `maxservers' domain controllers from `replies'. The 292 | `addresses' argument is the ordered list of addresses from DNS SRV 293 | resolution. It can be used to obtain SRV ordering information. 294 | """ 295 | local = [] 296 | remote = [] 297 | for reply in replies: 298 | assert hasattr(reply, 'checked') 299 | if not reply.checked: 300 | continue 301 | if self.m_site.lower() == reply.server_site.lower(): 302 | local.append(reply) 303 | else: 304 | remote.append(reply) 305 | local.sort(lambda x,y: cmp(addresses.index((x.q_hostname, x.q_port)), 306 | addresses.index((y.q_hostname, y.q_port)))) 307 | remote.sort(lambda x,y: cmp(x.q_timing, y.q_timing)) 308 | self.m_logger.debug('Local DCs: %s' % ', '.join(['%s:%s' % 309 | (x.q_hostname, x.q_port) for x in local])) 310 | self.m_logger.debug('Remote DCs: %s' % ', '.join(['%s:%s' % 311 | (x.q_hostname, x.q_port) for x in remote])) 312 | result = local + remote 313 | result = result[:maxservers] 314 | self.m_logger.debug('Selected DCs: %s' % ', '.join(['%s:%s' % 315 | (x.q_hostname, x.q_port) for x in result])) 316 | return result 317 | -------------------------------------------------------------------------------- /lib/ad/protocol/asn1.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | 9 | Boolean = 0x01 10 | Integer = 0x02 11 | OctetString = 0x04 12 | Null = 0x05 13 | ObjectIdentifier = 0x06 14 | Enumerated = 0x0a 15 | Sequence = 0x10 16 | Set = 0x11 17 | 18 | TypeConstructed = 0x20 19 | TypePrimitive = 0x00 20 | 21 | ClassUniversal = 0x00 22 | ClassApplication = 0x40 23 | ClassContext = 0x80 24 | ClassPrivate = 0xc0 25 | 26 | import re 27 | 28 | 29 | class Error(Exception): 30 | """ASN1 error""" 31 | 32 | 33 | class Encoder(object): 34 | """A ASN.1 encoder. Uses DER encoding.""" 35 | 36 | def __init__(self): 37 | """Constructor.""" 38 | self.m_stack = None 39 | 40 | def start(self): 41 | """Start encoding.""" 42 | self.m_stack = [[]] 43 | 44 | def enter(self, nr, cls=None): 45 | """Start a constructed data value.""" 46 | if self.m_stack is None: 47 | raise Error, 'Encoder not initialized. Call start() first.' 48 | if cls is None: 49 | cls = ClassUniversal 50 | self._emit_tag(nr, TypeConstructed, cls) 51 | self.m_stack.append([]) 52 | 53 | def leave(self): 54 | """Finish a constructed data value.""" 55 | if self.m_stack is None: 56 | raise Error, 'Encoder not initialized. Call start() first.' 57 | if len(self.m_stack) == 1: 58 | raise Error, 'Tag stack is empty.' 59 | value = ''.join(self.m_stack[-1]) 60 | del self.m_stack[-1] 61 | self._emit_length(len(value)) 62 | self._emit(value) 63 | 64 | def write(self, value, nr=None, typ=None, cls=None): 65 | """Write a primitive data value.""" 66 | if self.m_stack is None: 67 | raise Error, 'Encoder not initialized. Call start() first.' 68 | if nr is None: 69 | if isinstance(value, int) or isinstance(value, long): 70 | nr = Integer 71 | elif isinstance(value, str) or isinstance(value, unicode): 72 | nr = OctetString 73 | elif value is None: 74 | nr = Null 75 | if typ is None: 76 | typ = TypePrimitive 77 | if cls is None: 78 | cls = ClassUniversal 79 | value = self._encode_value(nr, value) 80 | self._emit_tag(nr, typ, cls) 81 | self._emit_length(len(value)) 82 | self._emit(value) 83 | 84 | def output(self): 85 | """Return the encoded output.""" 86 | if self.m_stack is None: 87 | raise Error, 'Encoder not initialized. Call start() first.' 88 | if len(self.m_stack) != 1: 89 | raise Error, 'Stack is not empty.' 90 | output = ''.join(self.m_stack[0]) 91 | return output 92 | 93 | def _emit_tag(self, nr, typ, cls): 94 | """Emit a tag.""" 95 | if nr < 31: 96 | self._emit_tag_short(nr, typ, cls) 97 | else: 98 | self._emit_tag_long(nr, typ, cls) 99 | 100 | def _emit_tag_short(self, nr, typ, cls): 101 | """Emit a short (< 31 bytes) tag.""" 102 | assert nr < 31 103 | self._emit(chr(nr | typ | cls)) 104 | 105 | def _emit_tag_long(self, nr, typ, cls): 106 | """Emit a long (>= 31 bytes) tag.""" 107 | head = chr(typ | cls | 0x1f) 108 | self._emit(head) 109 | values = [] 110 | values.append((nr & 0x7f)) 111 | nr >>= 7 112 | while nr: 113 | values.append((nr & 0x7f) | 0x80) 114 | nr >>= 7 115 | values.reverse() 116 | values = map(chr, values) 117 | for val in values: 118 | self._emit(val) 119 | 120 | def _emit_length(self, length): 121 | """Emit length octects.""" 122 | if length < 128: 123 | self._emit_length_short(length) 124 | else: 125 | self._emit_length_long(length) 126 | 127 | def _emit_length_short(self, length): 128 | """Emit the short length form (< 128 octets).""" 129 | assert length < 128 130 | self._emit(chr(length)) 131 | 132 | def _emit_length_long(self, length): 133 | """Emit the long length form (>= 128 octets).""" 134 | values = [] 135 | while length: 136 | values.append(length & 0xff) 137 | length >>= 8 138 | values.reverse() 139 | values = map(chr, values) 140 | # really for correctness as this should not happen anytime soon 141 | assert len(values) < 127 142 | head = chr(0x80 | len(values)) 143 | self._emit(head) 144 | for val in values: 145 | self._emit(val) 146 | 147 | def _emit(self, s): 148 | """Emit raw bytes.""" 149 | assert isinstance(s, str) 150 | self.m_stack[-1].append(s) 151 | 152 | def _encode_value(self, nr, value): 153 | """Encode a value.""" 154 | if nr in (Integer, Enumerated): 155 | value = self._encode_integer(value) 156 | elif nr == OctetString: 157 | value = self._encode_octet_string(value) 158 | elif nr == Boolean: 159 | value = self._encode_boolean(value) 160 | elif nr == Null: 161 | value = self._encode_null() 162 | elif nr == ObjectIdentifier: 163 | value = self._encode_object_identifier(value) 164 | return value 165 | 166 | def _encode_boolean(self, value): 167 | """Encode a boolean.""" 168 | return value and '\xff' or '\x00' 169 | 170 | def _encode_integer(self, value): 171 | """Encode an integer.""" 172 | if value < 0: 173 | value = -value 174 | negative = True 175 | limit = 0x80 176 | else: 177 | negative = False 178 | limit = 0x7f 179 | values = [] 180 | while value > limit: 181 | values.append(value & 0xff) 182 | value >>= 8 183 | values.append(value & 0xff) 184 | if negative: 185 | # create two's complement 186 | for i in range(len(values)): 187 | values[i] = 0xff - values[i] 188 | for i in range(len(values)): 189 | values[i] += 1 190 | if values[i] <= 0xff: 191 | break 192 | assert i != len(values)-1 193 | values[i] = 0x00 194 | values.reverse() 195 | values = map(chr, values) 196 | return ''.join(values) 197 | 198 | def _encode_octet_string(self, value): 199 | """Encode an octetstring.""" 200 | # Use the primitive encoding 201 | return value 202 | 203 | def _encode_null(self): 204 | """Encode a Null value.""" 205 | return '' 206 | 207 | _re_oid = re.compile('^[0-9]+(\.[0-9]+)+$') 208 | 209 | def _encode_object_identifier(self, oid): 210 | """Encode an object identifier.""" 211 | if not self._re_oid.match(oid): 212 | raise Error, 'Illegal object identifier' 213 | cmps = map(int, oid.split('.')) 214 | if cmps[0] > 39 or cmps[1] > 39: 215 | raise Error, 'Illegal object identifier' 216 | cmps = [40 * cmps[0] + cmps[1]] + cmps[2:] 217 | cmps.reverse() 218 | result = [] 219 | for cmp in cmps: 220 | result.append(cmp & 0x7f) 221 | while cmp > 0x7f: 222 | cmp >>= 7 223 | result.append(0x80 | (cmp & 0x7f)) 224 | result.reverse() 225 | result = map(chr, result) 226 | return ''.join(result) 227 | 228 | 229 | class Decoder(object): 230 | """A ASN.1 decoder. Understands BER (and DER which is a subset).""" 231 | 232 | def __init__(self): 233 | """Constructor.""" 234 | self.m_stack = None 235 | self.m_tag = None 236 | 237 | def start(self, data): 238 | """Start processing `data'.""" 239 | if not isinstance(data, str): 240 | raise Error, 'Expecting string instance.' 241 | self.m_stack = [[0, data]] 242 | self.m_tag = None 243 | 244 | def peek(self): 245 | """Return the value of the next tag without moving to the next 246 | TLV record.""" 247 | if self.m_stack is None: 248 | raise Error, 'No input selected. Call start() first.' 249 | if self._end_of_input(): 250 | return None 251 | if self.m_tag is None: 252 | self.m_tag = self._read_tag() 253 | return self.m_tag 254 | 255 | def read(self): 256 | """Read a simple value and move to the next TLV record.""" 257 | if self.m_stack is None: 258 | raise Error, 'No input selected. Call start() first.' 259 | if self._end_of_input(): 260 | return None 261 | tag = self.peek() 262 | length = self._read_length() 263 | value = self._read_value(tag[0], length) 264 | self.m_tag = None 265 | return (tag, value) 266 | 267 | def eof(self): 268 | """Return True if we are end of input.""" 269 | return self._end_of_input() 270 | 271 | def enter(self): 272 | """Enter a constructed tag.""" 273 | if self.m_stack is None: 274 | raise Error, 'No input selected. Call start() first.' 275 | nr, typ, cls = self.peek() 276 | if typ != TypeConstructed: 277 | raise Error, 'Cannot enter a non-constructed tag.' 278 | length = self._read_length() 279 | bytes = self._read_bytes(length) 280 | self.m_stack.append([0, bytes]) 281 | self.m_tag = None 282 | 283 | def leave(self): 284 | """Leave the last entered constructed tag.""" 285 | if self.m_stack is None: 286 | raise Error, 'No input selected. Call start() first.' 287 | if len(self.m_stack) == 1: 288 | raise Error, 'Tag stack is empty.' 289 | del self.m_stack[-1] 290 | self.m_tag = None 291 | 292 | def _decode_boolean(self, bytes): 293 | """Decode a boolean value.""" 294 | if len(bytes) != 1: 295 | raise Error, 'ASN1 syntax error' 296 | if bytes[0] == '\x00': 297 | return False 298 | return True 299 | 300 | def _read_tag(self): 301 | """Read a tag from the input.""" 302 | byte = self._read_byte() 303 | cls = byte & 0xc0 304 | typ = byte & 0x20 305 | nr = byte & 0x1f 306 | if nr == 0x1f: 307 | nr = 0 308 | while True: 309 | byte = self._read_byte() 310 | nr = (nr << 7) | (byte & 0x7f) 311 | if not byte & 0x80: 312 | break 313 | return (nr, typ, cls) 314 | 315 | def _read_length(self): 316 | """Read a length from the input.""" 317 | byte = self._read_byte() 318 | if byte & 0x80: 319 | count = byte & 0x7f 320 | if count == 0x7f: 321 | raise Error, 'ASN1 syntax error' 322 | bytes = self._read_bytes(count) 323 | bytes = [ ord(b) for b in bytes ] 324 | length = 0L 325 | for byte in bytes: 326 | length = (length << 8) | byte 327 | try: 328 | length = int(length) 329 | except OverflowError: 330 | pass 331 | else: 332 | length = byte 333 | return length 334 | 335 | def _read_value(self, nr, length): 336 | """Read a value from the input.""" 337 | bytes = self._read_bytes(length) 338 | if nr == Boolean: 339 | value = self._decode_boolean(bytes) 340 | elif nr in (Integer, Enumerated): 341 | value = self._decode_integer(bytes) 342 | elif nr == OctetString: 343 | value = self._decode_octet_string(bytes) 344 | elif nr == Null: 345 | value = self._decode_null(bytes) 346 | elif nr == ObjectIdentifier: 347 | value = self._decode_object_identifier(bytes) 348 | else: 349 | value = bytes 350 | return value 351 | 352 | def _read_byte(self): 353 | """Return the next input byte, or raise an error on end-of-input.""" 354 | index, input = self.m_stack[-1] 355 | try: 356 | byte = ord(input[index]) 357 | except IndexError: 358 | raise Error, 'Premature end of input.' 359 | self.m_stack[-1][0] += 1 360 | return byte 361 | 362 | def _read_bytes(self, count): 363 | """Return the next `count' bytes of input. Raise error on 364 | end-of-input.""" 365 | index, input = self.m_stack[-1] 366 | bytes = input[index:index+count] 367 | if len(bytes) != count: 368 | raise Error, 'Premature end of input.' 369 | self.m_stack[-1][0] += count 370 | return bytes 371 | 372 | def _end_of_input(self): 373 | """Return True if we are at the end of input.""" 374 | index, input = self.m_stack[-1] 375 | assert not index > len(input) 376 | return index == len(input) 377 | 378 | def _decode_integer(self, bytes): 379 | """Decode an integer value.""" 380 | values = [ ord(b) for b in bytes ] 381 | # check if the integer is normalized 382 | if len(values) > 1 and \ 383 | (values[0] == 0xff and values[1] & 0x80 or 384 | values[0] == 0x00 and not (values[1] & 0x80)): 385 | raise Error, 'ASN1 syntax error' 386 | negative = values[0] & 0x80 387 | if negative: 388 | # make positive by taking two's complement 389 | for i in range(len(values)): 390 | values[i] = 0xff - values[i] 391 | for i in range(len(values)-1, -1, -1): 392 | values[i] += 1 393 | if values[i] <= 0xff: 394 | break 395 | assert i > 0 396 | values[i] = 0x00 397 | value = 0L 398 | for val in values: 399 | value = (value << 8) | val 400 | if negative: 401 | value = -value 402 | try: 403 | value = int(value) 404 | except OverflowError: 405 | pass 406 | return value 407 | 408 | def _decode_octet_string(self, bytes): 409 | """Decode an octet string.""" 410 | return bytes 411 | 412 | def _decode_null(self, bytes): 413 | """Decode a Null value.""" 414 | if len(bytes) != 0: 415 | raise Error, 'ASN1 syntax error' 416 | return None 417 | 418 | def _decode_object_identifier(self, bytes): 419 | """Decode an object identifier.""" 420 | result = [] 421 | value = 0 422 | for i in range(len(bytes)): 423 | byte = ord(bytes[i]) 424 | if value == 0 and byte == 0x80: 425 | raise Error, 'ASN1 syntax error' 426 | value = (value << 7) | (byte & 0x7f) 427 | if not byte & 0x80: 428 | result.append(value) 429 | value = 0 430 | if len(result) == 0 or result[0] > 1599: 431 | raise Error, 'ASN1 syntax error' 432 | result = [result[0] // 40, result[0] % 40] + result[1:] 433 | result = map(str, result) 434 | return '.'.join(result) 435 | -------------------------------------------------------------------------------- /lib/ad/core/test/test_client.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007 by the Python-AD authors. See the file 7 | # "AUTHORS" for a complete overview. 8 | 9 | from nose.tools import assert_raises 10 | 11 | from ad.test.base import BaseTest 12 | from ad.core.object import activate 13 | from ad.core.client import Client 14 | from ad.core.locate import Locator 15 | from ad.core.constant import * 16 | from ad.core.creds import Creds 17 | from ad.core.exception import Error as ADError, LDAPError 18 | 19 | 20 | class TestADClient(BaseTest): 21 | """Test suite for ADClient""" 22 | 23 | def test_search(self): 24 | self.require(ad_user=True) 25 | domain = self.domain() 26 | creds = Creds(domain) 27 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 28 | activate(creds) 29 | client = Client(domain) 30 | result = client.search('(objectClass=user)') 31 | assert len(result) > 1 32 | 33 | def _delete_user(self, client, name, server=None): 34 | # Delete any user that may conflict with a newly to be created user 35 | filter = '(|(cn=%s)(sAMAccountName=%s)(userPrincipalName=%s))' % \ 36 | (name, name, '%s@%s' % (name, client.domain().upper())) 37 | result = client.search('(&(objectClass=user)(sAMAccountName=%s))' % name, 38 | server=server) 39 | for res in result: 40 | client.delete(res[0], server=server) 41 | 42 | def _create_user(self, client, name, server=None): 43 | attrs = [] 44 | attrs.append(('cn', [name])) 45 | attrs.append(('sAMAccountName', [name])) 46 | attrs.append(('userPrincipalName', ['%s@%s' % (name, client.domain().upper())])) 47 | ctrl = AD_USERCTRL_ACCOUNT_DISABLED | AD_USERCTRL_NORMAL_ACCOUNT 48 | attrs.append(('userAccountControl', [str(ctrl)])) 49 | attrs.append(('objectClass', ['user'])) 50 | dn = 'cn=%s,cn=users,%s' % (name, client.dn_from_domain_name(client.domain())) 51 | self._delete_user(client, name, server=server) 52 | client.add(dn, attrs, server=server) 53 | return dn 54 | 55 | def _delete_obj(self, client, dn, server=None): 56 | try: 57 | client.delete(dn, server=server) 58 | except (ADError, LDAPError): 59 | pass 60 | 61 | def _create_ou(self, client, name, server=None): 62 | attrs = [] 63 | attrs.append(('objectClass', ['organizationalUnit'])) 64 | attrs.append(('ou', [name])) 65 | dn = 'ou=%s,%s' % (name, client.dn_from_domain_name(client.domain())) 66 | self._delete_obj(client, dn, server=server) 67 | client.add(dn, attrs, server=server) 68 | return dn 69 | 70 | def test_add(self): 71 | self.require(ad_admin=True) 72 | domain = self.domain() 73 | creds = Creds(domain) 74 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 75 | activate(creds) 76 | client = Client(domain) 77 | user = self._create_user(client, 'test-usr') 78 | self._delete_obj(client, user) 79 | 80 | def test_delete(self): 81 | self.require(ad_admin=True) 82 | domain = self.domain() 83 | creds = Creds(domain) 84 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 85 | activate(creds) 86 | client = Client(domain) 87 | dn = self._create_user(client, 'test-usr') 88 | client.delete(dn) 89 | 90 | def test_modify(self): 91 | self.require(ad_admin=True) 92 | domain = self.domain() 93 | creds = Creds(domain) 94 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 95 | activate(creds) 96 | client = Client(domain) 97 | user = self._create_user(client, 'test-usr') 98 | mods = [] 99 | mods.append(('replace', 'sAMAccountName', ['test-usr-2'])) 100 | client.modify(user, mods) 101 | self._delete_obj(client, user) 102 | 103 | def test_modrdn(self): 104 | self.require(ad_admin=True) 105 | domain = self.domain() 106 | creds = Creds(domain) 107 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 108 | activate(creds) 109 | client = Client(domain) 110 | result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') 111 | if result: 112 | client.delete(result[0][0]) 113 | user = self._create_user(client, 'test-usr') 114 | client.modrdn(user, 'cn=test-usr2') 115 | result = client.search('(&(objectClass=user)(cn=test-usr2))') 116 | assert len(result) == 1 117 | 118 | def test_rename(self): 119 | self.require(ad_admin=True) 120 | domain = self.domain() 121 | creds = Creds(domain) 122 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 123 | activate(creds) 124 | client = Client(domain) 125 | result = client.search('(&(objectClass=user)(sAMAccountName=test-usr))') 126 | if result: 127 | client.delete(result[0][0]) 128 | user = self._create_user(client, 'test-usr') 129 | client.rename(user, 'cn=test-usr2') 130 | result = client.search('(&(objectClass=user)(cn=test-usr2))') 131 | assert len(result) == 1 132 | user = result[0][0] 133 | ou = self._create_ou(client, 'test-ou') 134 | client.rename(user, 'cn=test-usr', ou) 135 | newdn = 'cn=test-usr,%s' % ou 136 | result = client.search('(&(objectClass=user)(cn=test-usr))') 137 | assert len(result) == 1 138 | assert result[0][0].lower() == newdn.lower() 139 | 140 | def test_forest(self): 141 | self.require(ad_user=True) 142 | domain = self.domain() 143 | creds = Creds(domain) 144 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 145 | activate(creds) 146 | client = Client(domain) 147 | forest = client.forest() 148 | assert forest 149 | assert forest.isupper() 150 | 151 | def test_domains(self): 152 | self.require(ad_user=True) 153 | domain = self.domain() 154 | creds = Creds(domain) 155 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 156 | activate(creds) 157 | client = Client(domain) 158 | domains = client.domains() 159 | for domain in domains: 160 | assert domain 161 | assert domain.isupper() 162 | 163 | def test_naming_contexts(self): 164 | self.require(ad_user=True) 165 | domain = self.domain() 166 | creds = Creds(domain) 167 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 168 | activate(creds) 169 | client = Client(domain) 170 | naming_contexts = client.naming_contexts() 171 | assert len(naming_contexts) >= 3 172 | 173 | def test_search_all_domains(self): 174 | self.require(ad_user=True) 175 | domain = self.domain() 176 | creds = Creds(domain) 177 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 178 | activate(creds) 179 | client = Client(domain) 180 | domains = client.domains() 181 | for domain in domains: 182 | base = client.dn_from_domain_name(domain) 183 | result = client.search('(objectClass=*)', base=base, scope='base') 184 | assert len(result) == 1 185 | 186 | def test_search_schema(self): 187 | self.require(ad_user=True) 188 | domain = self.domain() 189 | creds = Creds(domain) 190 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 191 | activate(creds) 192 | client = Client(domain) 193 | base = client.schema_base() 194 | result = client.search('(objectClass=*)', base=base, scope='base') 195 | assert len(result) == 1 196 | 197 | def test_search_configuration(self): 198 | self.require(ad_user=True) 199 | domain = self.domain() 200 | creds = Creds(domain) 201 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 202 | activate(creds) 203 | client = Client(domain) 204 | base = client.configuration_base() 205 | result = client.search('(objectClass=*)', base=base, scope='base') 206 | assert len(result) == 1 207 | 208 | def _delete_group(self, client, dn, server=None): 209 | try: 210 | client.delete(dn, server=server) 211 | except (ADError, LDAPError): 212 | pass 213 | 214 | def _create_group(self, client, name, server=None): 215 | attrs = [] 216 | attrs.append(('cn', [name])) 217 | attrs.append(('sAMAccountName', [name])) 218 | attrs.append(('objectClass', ['group'])) 219 | dn = 'cn=%s,cn=Users,%s' % (name, client.dn_from_domain_name(client.domain())) 220 | self._delete_group(client, dn, server=server) 221 | client.add(dn, attrs, server=server) 222 | return dn 223 | 224 | def _add_user_to_group(self, client, user, group): 225 | mods = [] 226 | mods.append(('delete', 'member', [user])) 227 | try: 228 | client.modify(group, mods) 229 | except (ADError, LDAPError): 230 | pass 231 | mods = [] 232 | mods.append(('add', 'member', [user])) 233 | client.modify(group, mods) 234 | 235 | def test_incremental_retrieval_of_multivalued_attributes(self): 236 | self.require(ad_admin=True, expensive=True) 237 | domain = self.domain() 238 | creds = Creds(domain) 239 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 240 | activate(creds) 241 | client = Client(domain) 242 | user = self._create_user(client, 'test-usr') 243 | groups = [] 244 | for i in range(2000): 245 | group = self._create_group(client, 'test-grp-%04d' % i) 246 | self._add_user_to_group(client, user, group) 247 | groups.append(group) 248 | result = client.search('(sAMAccountName=test-usr)') 249 | assert len(result) == 1 250 | dn, attrs = result[0] 251 | assert attrs.has_key('memberOf') 252 | assert len(attrs['memberOf']) == 2000 253 | self._delete_obj(client, user) 254 | for group in groups: 255 | self._delete_group(client, group) 256 | 257 | def test_paged_results(self): 258 | self.require(ad_admin=True, expensive=True) 259 | domain = self.domain() 260 | creds = Creds(domain) 261 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 262 | activate(creds) 263 | client = Client(domain) 264 | users = [] 265 | for i in range(2000): 266 | user = self._create_user(client, 'test-usr-%04d' % i) 267 | users.append(user) 268 | result = client.search('(cn=test-usr-*)') 269 | assert len(result) == 2000 270 | for user in users: 271 | self._delete_obj(client, user) 272 | 273 | def test_search_rootdse(self): 274 | self.require(ad_user=True) 275 | domain = self.domain() 276 | creds = Creds(domain) 277 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 278 | activate(creds) 279 | locator = Locator() 280 | server = locator.locate(domain) 281 | client = Client(domain) 282 | result = client.search(base='', scope='base', server=server) 283 | assert len(result) == 1 284 | dns, attrs = result[0] 285 | assert attrs.has_key('supportedControl') 286 | assert attrs.has_key('supportedSASLMechanisms') 287 | 288 | def test_search_server(self): 289 | self.require(ad_user=True) 290 | domain = self.domain() 291 | creds = Creds(domain) 292 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 293 | activate(creds) 294 | locator = Locator() 295 | server = locator.locate(domain) 296 | client = Client(domain) 297 | result = client.search('(objectClass=user)', server=server) 298 | assert len(result) > 1 299 | 300 | def test_search_gc(self): 301 | self.require(ad_user=True) 302 | domain = self.domain() 303 | creds = Creds(domain) 304 | creds.acquire(self.ad_user_account(), self.ad_user_password()) 305 | activate(creds) 306 | client = Client(domain) 307 | result = client.search('(objectClass=user)', scheme='gc') 308 | assert len(result) > 1 309 | for res in result: 310 | dn, attrs = res 311 | # accountExpires is always set, but is not a GC attribute 312 | assert 'accountExpires' not in attrs 313 | 314 | def test_set_password(self): 315 | self.require(ad_admin=True) 316 | domain = self.domain() 317 | creds = Creds(domain) 318 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 319 | activate(creds) 320 | client = Client(domain) 321 | user = self._create_user(client, 'test-usr-1') 322 | principal = 'test-usr-1@%s' % domain 323 | client.set_password(principal, 'Pass123') 324 | mods = [] 325 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT 326 | mods.append(('replace', 'userAccountControl', [str(ctrl)])) 327 | client.modify(user, mods) 328 | creds = Creds(domain) 329 | creds.acquire('test-usr-1', 'Pass123') 330 | assert_raises(ADError, creds.acquire, 'test-usr-1', 'Pass321') 331 | self._delete_obj(client, user) 332 | 333 | def test_set_password_target_pdc(self): 334 | self.require(ad_admin=True) 335 | domain = self.domain() 336 | creds = Creds(domain) 337 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 338 | activate(creds) 339 | client = Client(domain) 340 | locator = Locator() 341 | pdc = locator.locate(domain, role='pdc') 342 | user = self._create_user(client, 'test-usr-2', server=pdc) 343 | principal = 'test-usr-2@%s' % domain 344 | client.set_password(principal, 'Pass123', server=pdc) 345 | mods = [] 346 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT 347 | mods.append(('replace', 'userAccountControl', [str(ctrl)])) 348 | client.modify(user, mods, server=pdc) 349 | creds = Creds(domain) 350 | creds.acquire('test-usr-2', 'Pass123', server=pdc) 351 | assert_raises(ADError, creds.acquire, 'test-usr-2','Pass321', 352 | server=pdc) 353 | self._delete_obj(client, user, server=pdc) 354 | 355 | def test_change_password(self): 356 | self.require(ad_admin=True) 357 | domain = self.domain() 358 | creds = Creds(domain) 359 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 360 | activate(creds) 361 | client = Client(domain) 362 | user = self._create_user(client, 'test-usr-3') 363 | principal = 'test-usr-3@%s' % domain 364 | client.set_password(principal, 'Pass123') 365 | mods = [] 366 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT 367 | mods.append(('replace', 'userAccountControl', [str(ctrl)])) 368 | mods.append(('replace', 'pwdLastSet', ['0'])) 369 | client.modify(user, mods) 370 | client.change_password(principal, 'Pass123', 'Pass456') 371 | creds = Creds(domain) 372 | creds.acquire('test-usr-3', 'Pass456') 373 | assert_raises(ADError, creds.acquire, 'test-usr-3', 'Pass321') 374 | self._delete_obj(client, user) 375 | 376 | def test_change_password_target_pdc(self): 377 | self.require(ad_admin=True) 378 | domain = self.domain() 379 | creds = Creds(domain) 380 | creds.acquire(self.ad_admin_account(), self.ad_admin_password()) 381 | activate(creds) 382 | client = Client(domain) 383 | locator = Locator() 384 | pdc = locator.locate(domain, role='pdc') 385 | user = self._create_user(client, 'test-usr-4', server=pdc) 386 | principal = 'test-usr-4@%s' % domain 387 | client.set_password(principal, 'Pass123', server=pdc) 388 | mods = [] 389 | ctrl = AD_USERCTRL_NORMAL_ACCOUNT 390 | mods.append(('replace', 'userAccountControl', [str(ctrl)])) 391 | mods.append(('replace', 'pwdLastSet', ['0'])) 392 | client.modify(user, mods, server=pdc) 393 | client.change_password(principal, 'Pass123', 'Pass456', server=pdc) 394 | creds = Creds(domain) 395 | creds.acquire('test-usr-4', 'Pass456', server=pdc) 396 | assert_raises(ADError, creds.acquire, 'test-usr-4', 'Pass321', 397 | server=pdc) 398 | self._delete_obj(client, user, server=pdc) 399 | -------------------------------------------------------------------------------- /lib/ad/protocol/test/test_asn1.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Python-AD. Python-AD is free software that is made 3 | # available under the MIT license. Consult the file "LICENSE" that is 4 | # distributed together with this file for the exact licensing terms. 5 | # 6 | # Python-AD is copyright (c) 2007-2008 by the Python-AD authors. See the 7 | # file "AUTHORS" for a complete overview. 8 | 9 | from ad.protocol import asn1 10 | from nose.tools import assert_raises 11 | 12 | 13 | class TestEncoder(object): 14 | """Test suite for ASN1 Encoder.""" 15 | 16 | def test_boolean(self): 17 | enc = asn1.Encoder() 18 | enc.start() 19 | enc.write(True, asn1.Boolean) 20 | res = enc.output() 21 | assert res == '\x01\x01\xff' 22 | 23 | def test_integer(self): 24 | enc = asn1.Encoder() 25 | enc.start() 26 | enc.write(1) 27 | res = enc.output() 28 | assert res == '\x02\x01\x01' 29 | 30 | def test_long_integer(self): 31 | enc = asn1.Encoder() 32 | enc.start() 33 | enc.write(0x0102030405060708090a0b0c0d0e0fL) 34 | res = enc.output() 35 | assert res == '\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' 36 | 37 | def test_negative_integer(self): 38 | enc = asn1.Encoder() 39 | enc.start() 40 | enc.write(-1) 41 | res = enc.output() 42 | assert res == '\x02\x01\xff' 43 | 44 | def test_long_negative_integer(self): 45 | enc = asn1.Encoder() 46 | enc.start() 47 | enc.write(-0x0102030405060708090a0b0c0d0e0fL) 48 | res = enc.output() 49 | assert res == '\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' 50 | 51 | def test_twos_complement_boundaries(self): 52 | enc = asn1.Encoder() 53 | enc.start() 54 | enc.write(127) 55 | res = enc.output() 56 | assert res == '\x02\x01\x7f' 57 | enc.start() 58 | enc.write(128) 59 | res = enc.output() 60 | assert res == '\x02\x02\x00\x80' 61 | enc.start() 62 | enc.write(-128) 63 | res = enc.output() 64 | assert res == '\x02\x01\x80' 65 | enc.start() 66 | enc.write(-129) 67 | res = enc.output() 68 | assert res == '\x02\x02\xff\x7f' 69 | 70 | def test_octet_string(self): 71 | enc = asn1.Encoder() 72 | enc.start() 73 | enc.write('foo') 74 | res = enc.output() 75 | assert res == '\x04\x03foo' 76 | 77 | def test_null(self): 78 | enc = asn1.Encoder() 79 | enc.start() 80 | enc.write(None) 81 | res = enc.output() 82 | assert res == '\x05\x00' 83 | 84 | def test_object_identifier(self): 85 | enc = asn1.Encoder() 86 | enc.start() 87 | enc.write('1.2.3', asn1.ObjectIdentifier) 88 | res = enc.output() 89 | assert res == '\x06\x02\x2a\x03' 90 | 91 | def test_long_object_identifier(self): 92 | enc = asn1.Encoder() 93 | enc.start() 94 | enc.write('39.2.3', asn1.ObjectIdentifier) 95 | res = enc.output() 96 | assert res == '\x06\x03\x8c\x1a\x03' 97 | enc.start() 98 | enc.write('1.39.3', asn1.ObjectIdentifier) 99 | res = enc.output() 100 | assert res == '\x06\x02\x4f\x03' 101 | enc.start() 102 | enc.write('1.2.300000', asn1.ObjectIdentifier) 103 | res = enc.output() 104 | assert res == '\x06\x04\x2a\x92\xa7\x60' 105 | 106 | def test_real_object_identifier(self): 107 | enc = asn1.Encoder() 108 | enc.start() 109 | enc.write('1.2.840.113554.1.2.1.1', asn1.ObjectIdentifier) 110 | res = enc.output() 111 | assert res == '\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' 112 | 113 | def test_enumerated(self): 114 | enc = asn1.Encoder() 115 | enc.start() 116 | enc.write(1, asn1.Enumerated) 117 | res = enc.output() 118 | assert res == '\x0a\x01\x01' 119 | 120 | def test_sequence(self): 121 | enc = asn1.Encoder() 122 | enc.start() 123 | enc.enter(asn1.Sequence) 124 | enc.write(1) 125 | enc.write('foo') 126 | enc.leave() 127 | res = enc.output() 128 | assert res == '\x30\x08\x02\x01\x01\x04\x03foo' 129 | 130 | def test_sequence_of(self): 131 | enc = asn1.Encoder() 132 | enc.start() 133 | enc.enter(asn1.Sequence) 134 | enc.write(1) 135 | enc.write(2) 136 | enc.leave() 137 | res = enc.output() 138 | assert res == '\x30\x06\x02\x01\x01\x02\x01\x02' 139 | 140 | def test_set(self): 141 | enc = asn1.Encoder() 142 | enc.start() 143 | enc.enter(asn1.Set) 144 | enc.write(1) 145 | enc.write('foo') 146 | enc.leave() 147 | res = enc.output() 148 | assert res == '\x31\x08\x02\x01\x01\x04\x03foo' 149 | 150 | def test_set_of(self): 151 | enc = asn1.Encoder() 152 | enc.start() 153 | enc.enter(asn1.Set) 154 | enc.write(1) 155 | enc.write(2) 156 | enc.leave() 157 | res = enc.output() 158 | assert res == '\x31\x06\x02\x01\x01\x02\x01\x02' 159 | 160 | def test_context(self): 161 | enc = asn1.Encoder() 162 | enc.start() 163 | enc.enter(1, asn1.ClassContext) 164 | enc.write(1) 165 | enc.leave() 166 | res = enc.output() 167 | assert res == '\xa1\x03\x02\x01\x01' 168 | 169 | def test_application(self): 170 | enc = asn1.Encoder() 171 | enc.start() 172 | enc.enter(1, asn1.ClassApplication) 173 | enc.write(1) 174 | enc.leave() 175 | res = enc.output() 176 | assert res == '\x61\x03\x02\x01\x01' 177 | 178 | def test_private(self): 179 | enc = asn1.Encoder() 180 | enc.start() 181 | enc.enter(1, asn1.ClassPrivate) 182 | enc.write(1) 183 | enc.leave() 184 | res = enc.output() 185 | assert res == '\xe1\x03\x02\x01\x01' 186 | 187 | def test_long_tag_id(self): 188 | enc = asn1.Encoder() 189 | enc.start() 190 | enc.enter(0xffff) 191 | enc.write(1) 192 | enc.leave() 193 | res = enc.output() 194 | assert res == '\x3f\x83\xff\x7f\x03\x02\x01\x01' 195 | 196 | def test_long_tag_length(self): 197 | enc = asn1.Encoder() 198 | enc.start() 199 | enc.write('x' * 0xffff) 200 | res = enc.output() 201 | assert res == '\x04\x82\xff\xff' + 'x' * 0xffff 202 | 203 | def test_error_init(self): 204 | enc = asn1.Encoder() 205 | assert_raises(asn1.Error, enc.enter, asn1.Sequence) 206 | assert_raises(asn1.Error, enc.leave) 207 | assert_raises(asn1.Error, enc.write, 1) 208 | assert_raises(asn1.Error, enc.output) 209 | 210 | def test_error_stack(self): 211 | enc = asn1.Encoder() 212 | enc.start() 213 | assert_raises(asn1.Error, enc.leave) 214 | enc.enter(asn1.Sequence) 215 | assert_raises(asn1.Error, enc.output) 216 | enc.leave() 217 | assert_raises(asn1.Error, enc.leave) 218 | 219 | def test_error_object_identifier(self): 220 | enc = asn1.Encoder() 221 | enc.start() 222 | assert_raises(asn1.Error, enc.write, '1', asn1.ObjectIdentifier) 223 | assert_raises(asn1.Error, enc.write, '40.2.3', asn1.ObjectIdentifier) 224 | assert_raises(asn1.Error, enc.write, '1.40.3', asn1.ObjectIdentifier) 225 | assert_raises(asn1.Error, enc.write, '1.2.3.', asn1.ObjectIdentifier) 226 | assert_raises(asn1.Error, enc.write, '.1.2.3', asn1.ObjectIdentifier) 227 | assert_raises(asn1.Error, enc.write, 'foo', asn1.ObjectIdentifier) 228 | assert_raises(asn1.Error, enc.write, 'foo.bar', asn1.ObjectIdentifier) 229 | 230 | 231 | class TestDecoder(object): 232 | """Test suite for ASN1 Decoder.""" 233 | 234 | def test_boolean(self): 235 | buf = '\x01\x01\xff' 236 | dec = asn1.Decoder() 237 | dec.start(buf) 238 | tag = dec.peek() 239 | assert tag == (asn1.Boolean, asn1.TypePrimitive, asn1.ClassUniversal) 240 | tag, val = dec.read() 241 | assert isinstance(val, int) 242 | assert val == True 243 | buf = '\x01\x01\x01' 244 | dec.start(buf) 245 | tag, val = dec.read() 246 | assert isinstance(val, int) 247 | assert val == True 248 | buf = '\x01\x01\x00' 249 | dec.start(buf) 250 | tag, val = dec.read() 251 | assert isinstance(val, int) 252 | assert val == False 253 | 254 | def test_integer(self): 255 | buf = '\x02\x01\x01' 256 | dec = asn1.Decoder() 257 | dec.start(buf) 258 | tag = dec.peek() 259 | assert tag == (asn1.Integer, asn1.TypePrimitive, asn1.ClassUniversal) 260 | tag, val = dec.read() 261 | assert isinstance(val, int) 262 | assert val == 1 263 | 264 | def test_long_integer(self): 265 | buf = '\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' 266 | dec = asn1.Decoder() 267 | dec.start(buf) 268 | tag, val = dec.read() 269 | assert val == 0x0102030405060708090a0b0c0d0e0fL 270 | 271 | def test_negative_integer(self): 272 | buf = '\x02\x01\xff' 273 | dec = asn1.Decoder() 274 | dec.start(buf) 275 | tag, val = dec.read() 276 | assert val == -1 277 | 278 | def test_long_negative_integer(self): 279 | buf = '\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1' 280 | dec = asn1.Decoder() 281 | dec.start(buf) 282 | tag, val = dec.read() 283 | assert val == -0x0102030405060708090a0b0c0d0e0fL 284 | 285 | def test_twos_complement_boundaries(self): 286 | buf = '\x02\x01\x7f' 287 | dec = asn1.Decoder() 288 | dec.start(buf) 289 | tag, val = dec.read() 290 | assert val == 127 291 | buf = '\x02\x02\x00\x80' 292 | dec.start(buf) 293 | tag, val = dec.read() 294 | assert val == 128 295 | buf = '\x02\x01\x80' 296 | dec.start(buf) 297 | tag, val = dec.read() 298 | assert val == -128 299 | buf = '\x02\x02\xff\x7f' 300 | dec.start(buf) 301 | tag, val = dec.read() 302 | assert val == -129 303 | 304 | def test_octet_string(self): 305 | buf = '\x04\x03foo' 306 | dec = asn1.Decoder() 307 | dec.start(buf) 308 | tag = dec.peek() 309 | assert tag == (asn1.OctetString, asn1.TypePrimitive, asn1.ClassUniversal) 310 | tag, val = dec.read() 311 | assert isinstance(val, str) 312 | assert val == 'foo' 313 | 314 | def test_null(self): 315 | buf = '\x05\x00' 316 | dec = asn1.Decoder() 317 | dec.start(buf) 318 | tag = dec.peek() 319 | assert tag == (asn1.Null, asn1.TypePrimitive, asn1.ClassUniversal) 320 | tag, val = dec.read() 321 | assert val is None 322 | 323 | def test_object_identifier(self): 324 | dec = asn1.Decoder() 325 | buf = '\x06\x02\x2a\x03' 326 | dec.start(buf) 327 | tag = dec.peek() 328 | assert tag == (asn1.ObjectIdentifier, asn1.TypePrimitive, 329 | asn1.ClassUniversal) 330 | tag, val = dec.read() 331 | assert val == '1.2.3' 332 | 333 | def test_long_object_identifier(self): 334 | dec = asn1.Decoder() 335 | buf = '\x06\x03\x8c\x1a\x03' 336 | dec.start(buf) 337 | tag, val = dec.read() 338 | assert val == '39.2.3' 339 | buf = '\x06\x02\x4f\x03' 340 | dec.start(buf) 341 | tag, val = dec.read() 342 | assert val == '1.39.3' 343 | buf = '\x06\x04\x2a\x92\xa7\x60' 344 | dec.start(buf) 345 | tag, val = dec.read() 346 | assert val == '1.2.300000' 347 | 348 | def test_real_object_identifier(self): 349 | dec = asn1.Decoder() 350 | buf = '\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01' 351 | dec.start(buf) 352 | tag, val = dec.read() 353 | assert val == '1.2.840.113554.1.2.1.1' 354 | 355 | def test_enumerated(self): 356 | buf = '\x0a\x01\x01' 357 | dec = asn1.Decoder() 358 | dec.start(buf) 359 | tag = dec.peek() 360 | assert tag == (asn1.Enumerated, asn1.TypePrimitive, asn1.ClassUniversal) 361 | tag, val = dec.read() 362 | assert isinstance(val, int) 363 | assert val == 1 364 | 365 | def test_sequence(self): 366 | buf = '\x30\x08\x02\x01\x01\x04\x03foo' 367 | dec = asn1.Decoder() 368 | dec.start(buf) 369 | tag = dec.peek() 370 | assert tag == (asn1.Sequence, asn1.TypeConstructed, asn1.ClassUniversal) 371 | dec.enter() 372 | tag, val = dec.read() 373 | assert val == 1 374 | tag, val = dec.read() 375 | assert val == 'foo' 376 | 377 | def test_sequence_of(self): 378 | buf = '\x30\x06\x02\x01\x01\x02\x01\x02' 379 | dec = asn1.Decoder() 380 | dec.start(buf) 381 | tag = dec.peek() 382 | assert tag == (asn1.Sequence, asn1.TypeConstructed, asn1.ClassUniversal) 383 | dec.enter() 384 | tag, val = dec.read() 385 | assert val == 1 386 | tag, val = dec.read() 387 | assert val == 2 388 | 389 | def test_set(self): 390 | buf = '\x31\x08\x02\x01\x01\x04\x03foo' 391 | dec = asn1.Decoder() 392 | dec.start(buf) 393 | tag = dec.peek() 394 | assert tag == (asn1.Set, asn1.TypeConstructed, asn1.ClassUniversal) 395 | dec.enter() 396 | tag, val = dec.read() 397 | assert val == 1 398 | tag, val = dec.read() 399 | assert val == 'foo' 400 | 401 | def test_set_of(self): 402 | buf = '\x31\x06\x02\x01\x01\x02\x01\x02' 403 | dec = asn1.Decoder() 404 | dec.start(buf) 405 | tag = dec.peek() 406 | assert tag == (asn1.Set, asn1.TypeConstructed, asn1.ClassUniversal) 407 | dec.enter() 408 | tag, val = dec.read() 409 | assert val == 1 410 | tag, val = dec.read() 411 | assert val == 2 412 | 413 | def test_context(self): 414 | buf = '\xa1\x03\x02\x01\x01' 415 | dec = asn1.Decoder() 416 | dec.start(buf) 417 | tag = dec.peek() 418 | assert tag == (1, asn1.TypeConstructed, asn1.ClassContext) 419 | dec.enter() 420 | tag, val = dec.read() 421 | assert val == 1 422 | 423 | def test_application(self): 424 | buf = '\x61\x03\x02\x01\x01' 425 | dec = asn1.Decoder() 426 | dec.start(buf) 427 | tag = dec.peek() 428 | assert tag == (1, asn1.TypeConstructed, asn1.ClassApplication) 429 | dec.enter() 430 | tag, val = dec.read() 431 | assert val == 1 432 | 433 | def test_private(self): 434 | buf = '\xe1\x03\x02\x01\x01' 435 | dec = asn1.Decoder() 436 | dec.start(buf) 437 | tag = dec.peek() 438 | assert tag == (1, asn1.TypeConstructed, asn1.ClassPrivate) 439 | dec.enter() 440 | tag, val = dec.read() 441 | assert val == 1 442 | 443 | def test_long_tag_id(self): 444 | buf = '\x3f\x83\xff\x7f\x03\x02\x01\x01' 445 | dec = asn1.Decoder() 446 | dec.start(buf) 447 | tag = dec.peek() 448 | assert tag == (0xffff, asn1.TypeConstructed, asn1.ClassUniversal) 449 | dec.enter() 450 | tag, val = dec.read() 451 | assert val == 1 452 | 453 | def test_long_tag_length(self): 454 | buf = '\x04\x82\xff\xff' + 'x' * 0xffff 455 | dec = asn1.Decoder() 456 | dec.start(buf) 457 | tag, val = dec.read() 458 | assert val == 'x' * 0xffff 459 | 460 | def test_read_multiple(self): 461 | buf = '\x02\x01\x01\x02\x01\x02' 462 | dec = asn1.Decoder() 463 | dec.start(buf) 464 | tag, val = dec.read() 465 | assert val == 1 466 | tag, val = dec.read() 467 | assert val == 2 468 | assert dec.eof() 469 | 470 | def test_skip_primitive(self): 471 | buf = '\x02\x01\x01\x02\x01\x02' 472 | dec = asn1.Decoder() 473 | dec.start(buf) 474 | dec.read() 475 | tag, val = dec.read() 476 | assert val == 2 477 | assert dec.eof() 478 | 479 | def test_skip_constructed(self): 480 | buf = '\x30\x06\x02\x01\x01\x02\x01\x02\x02\x01\x03' 481 | dec = asn1.Decoder() 482 | dec.start(buf) 483 | dec.read() 484 | tag, val = dec.read() 485 | assert val == 3 486 | assert dec.eof() 487 | 488 | def test_error_init(self): 489 | dec = asn1.Decoder() 490 | assert_raises(asn1.Error, dec.peek) 491 | assert_raises(asn1.Error, dec.read) 492 | assert_raises(asn1.Error, dec.enter) 493 | assert_raises(asn1.Error, dec.leave) 494 | 495 | def test_error_stack(self): 496 | buf = '\x30\x08\x02\x01\x01\x04\x03foo' 497 | dec = asn1.Decoder() 498 | dec.start(buf) 499 | assert_raises(asn1.Error, dec.leave) 500 | dec.enter() 501 | dec.leave() 502 | assert_raises(asn1.Error, dec.leave) 503 | 504 | def test_no_input(self): 505 | dec = asn1.Decoder() 506 | dec.start('') 507 | tag = dec.peek() 508 | assert tag is None 509 | 510 | def test_error_missing_tag_bytes(self): 511 | buf = '\x3f' 512 | dec = asn1.Decoder() 513 | dec.start(buf) 514 | assert_raises(asn1.Error, dec.peek) 515 | buf = '\x3f\x83' 516 | dec.start(buf) 517 | assert_raises(asn1.Error, dec.peek) 518 | 519 | def test_error_no_length_bytes(self): 520 | buf = '\x02' 521 | dec = asn1.Decoder() 522 | dec.start(buf) 523 | assert_raises(asn1.Error, dec.read) 524 | 525 | def test_error_missing_length_bytes(self): 526 | buf = '\x04\x82\xff' 527 | dec = asn1.Decoder() 528 | dec.start(buf) 529 | assert_raises(asn1.Error, dec.read) 530 | 531 | def test_error_too_many_length_bytes(self): 532 | buf = '\x04\xff' + '\xff' * 0x7f 533 | dec = asn1.Decoder() 534 | dec.start(buf) 535 | assert_raises(asn1.Error, dec.read) 536 | 537 | def test_error_no_value_bytes(self): 538 | buf = '\x02\x01' 539 | dec = asn1.Decoder() 540 | dec.start(buf) 541 | assert_raises(asn1.Error, dec.read) 542 | 543 | def test_error_missing_value_bytes(self): 544 | buf = '\x02\x02\x01' 545 | dec = asn1.Decoder() 546 | dec.start(buf) 547 | assert_raises(asn1.Error, dec.read) 548 | 549 | def test_error_non_normalized_positive_integer(self): 550 | buf = '\x02\x02\x00\x01' 551 | dec = asn1.Decoder() 552 | dec.start(buf) 553 | assert_raises(asn1.Error, dec.read) 554 | 555 | def test_error_non_normalized_negative_integer(self): 556 | buf = '\x02\x02\xff\x80' 557 | dec = asn1.Decoder() 558 | dec.start(buf) 559 | assert_raises(asn1.Error, dec.read) 560 | 561 | def test_error_non_normalised_object_identifier(self): 562 | buf = '\x06\x02\x80\x01' 563 | dec = asn1.Decoder() 564 | dec.start(buf) 565 | assert_raises(asn1.Error, dec.read) 566 | 567 | def test_error_object_identifier_with_too_large_first_component(self): 568 | buf = '\x06\x02\x8c\x40' 569 | dec = asn1.Decoder() 570 | dec.start(buf) 571 | assert_raises(asn1.Error, dec.read) 572 | -------------------------------------------------------------------------------- /doc/reference.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API Reference 6 | 7 |
8 | 9 | Credential Management 10 | 11 | 12 | Most Active Directory operations require authentication. The default 13 | authentication mechanism in AD is Kerberos. Unfortunately, it has always 14 | been difficult to use Kerberos authentication with AD. It required for 15 | example to have a properly configured /etc/krb5.conf 16 | listing the realms you want to use and the Kerbers servers for those realms. 17 | 18 | 19 | 20 | Fortunately Python-AD simplifies AD credential management significantly. It 21 | does not require any system configuration and all functionality is embedded 22 | in a single class named Creds. This class is 23 | available from the Python package ad 24 | 25 | 26 | 27 | from ad import Creds 28 | 29 | 30 | 31 | The constructor for the Creds class takes one 32 | argument: domain. This parameter specifies the 33 | default AD domain for credential management. 34 | 35 | 36 | 37 | class Creds(object): 38 | """Credential management.""" 39 | 40 | def __init__(self, domain): 41 | """Constructor.""" 42 | 43 | 44 | 45 | The domain parameter is used as a default when 46 | principals are used without domain name. Credentials in any other domains 47 | can be acquired as long as the domain is part of the same AD forest. 48 | 49 | 50 | 51 | The Creds class has the following methods: 52 | 53 | 54 | 55 | 56 | def acquire(self, principal, password=None, keytab=None, server=None): 57 | """Acquire credentials.""" 58 | 59 | 60 | 61 | The acquire() function acquires credentials for 62 | principal principal. The principal can either a 63 | unqualified principal, in which case the default domain is assumed, or a 64 | principal in the form of user@domain in which case 65 | domain must be a domain in the same AD forest as the 66 | default domain. The password contains the password to 67 | the principal. If no password is given, a keytab will be used. This keytab 68 | is can be specified by the keytab parameter. If this 69 | parameter is not present either, the default system keytab is used. The 70 | server argument overrides the default Kerberos server 71 | to acquire credentials from. If this argument is not given, a suitable 72 | Kerberos server is autodetected. 73 | 74 | 75 | 76 | 77 | def load(self): 78 | """Load credentials from the OS.""" 79 | 80 | 81 | 82 | The function load() loads credentials from the default 83 | operating system credentials store. It raises an exception in case no 84 | credentials are available. 85 | 86 | 87 | 88 | 89 | def principal(self): 90 | """Return the current principal.""" 91 | 92 | 93 | 94 | The principal() method returns the current principal, 95 | i.e. the principal that was last succesfully used in a call to 96 | acquire(). 97 | 98 | 99 | 100 | 101 | def release(self): 102 | """Release all credentials.""" 103 | 104 | 105 | 106 | The release() method releases the currently held 107 | credentials. It is called automatically from the 108 | Creds destructor. 109 | 110 | 111 | 112 | Once credentials are acquired with acquire(), they need 113 | to be activated before they can be used. This is required because 114 | credentials are a process-global resource and they need to be installed in a 115 | place where other classes in Python-AD will be able to find them. Credential 116 | activation is done by calling a function called 117 | activate() as follows: 118 | 119 | 120 | 121 | from ad import activate 122 | activate(creds) 123 | 124 | 125 | 126 | The code fragment above will make the creds object the 127 | globally active credentials. The activate() can be 128 | called multiple times on different objects. The semantics are that the 129 | object on which activate() was called last will be the 130 | active credentials. If active credentials are released by calling the 131 | release() method of a Creds 132 | instance, then the credentials that were previously active are activated. If 133 | no previously activated credentials exist, there will be no credentials 134 | available after this call. 135 | 136 | 137 |
138 | 139 |
140 | 141 | AD Client Interface 142 | 143 | 144 | The actual operations on Active Directory are grouped in a single class 145 | called Client. This class is available from the 146 | ad package. 147 | 148 | 149 | 150 | from ad import Client 151 | 152 | 153 | 154 | The constructor to the Client class takes one 155 | argument: the domain. 156 | 157 | 158 | 159 | class Client(object): 160 | """AD client interface.""" 161 | 162 | def __init__(self, domain): 163 | """Constructor.""" 164 | 165 | 166 | 167 | The domain parameter sets the default domain for the 168 | Client class. As with the 169 | Creds class, the requirement to specify the domain 170 | does not mean that operations through the client are limited to the 171 | specified domain: all operations are available to any domain that is part of 172 | the same forest. 173 | 174 | 175 | 176 | Instances of the Client class will try to access the 177 | globally installed Creds instance. Therefore you must 178 | ensure that credentials are available before you use any functionality in 179 | this class. 180 | 181 | 182 | 183 | The following methods are provided by the Client 184 | class: 185 | 186 | 187 | 188 | 189 | def domain_name_from_dn(self, dn): 190 | """Given a DN, return a domain.""" 191 | 192 | 193 | 194 | The function domain_name_from_dn() is a utility 195 | function that, given an LDAP distinguished name, will return the Active 196 | Directory domain name. For example, if the dn 197 | parameter is set to cn=users,dc=freeadi,dc=org, the 198 | return value of this method will be FREEADI.ORG. 199 | 200 | 201 | 202 | 203 | def dn_from_domain_name(self, name): 204 | """Given a domain name, return a DN.""" 205 | 206 | 207 | 208 | The function dn_from_domain_name() is the inverse of 209 | domain_name_from_dn(). For example, if 210 | name is set to LINUX.FREEADI.ORG, 211 | the return value of this method will be 212 | dc=linux,dc=freeadi,dc=org. 213 | 214 | 215 | 216 | 217 | def domain(self): 218 | """Return the domain name of the current domain.""" 219 | 220 | 221 | 222 | This method returns the current default domain. 223 | 224 | 225 | 226 | 227 | def domain_base(self): 228 | """Return the base DN of the domain.""" 229 | 230 | 231 | 232 | The method domain_base() returns the LDAP base DN for 233 | the curent domain. If client is an instance of 234 | Client, then this is equivalent to 235 | client.dn_from_domain_name(client.domain()). 236 | 237 | 238 | 239 | def forest(self): 240 | """Return the domain name of the forest root.""" 241 | 242 | 243 | 244 | This method returns the root of the forest for the current domain. In case 245 | the current domain is a child domain, this will return something different 246 | than the domain() method. 247 | 248 | 249 | 250 | def forest_base(self): 251 | """Return the base DN of the forest root.""" 252 | 253 | 254 | 255 | The forest_base() method returns the LDAP base DN for 256 | the forest. This function is equivalent to 257 | client.dn_from_domain_name(client.forest()). 258 | 259 | 260 | 261 | def schema_base(self): 262 | """Return the base DN of the schema naming_context.""" 263 | 264 | 265 | 266 | This method returns the LDAP base DN for the schema naming context for the 267 | Active Directory forest. 268 | 269 | 270 | 271 | def configuration_base(self): 272 | """Return the base DN of the configuration naming_context.""" 273 | 274 | 275 | 276 | This method returns the LDAP base DN for the configuration namign context 277 | for the Active Directory forest. 278 | 279 | 280 | 281 | def naming_contexts(self): 282 | """Return a list of all naming_contexts.""" 283 | 284 | 285 | 286 | The naming_contexts() returns a list of all naming 287 | contexts in the forest. The term naming context is the Microsoft name for a 288 | directory partition. Each naming context is present on one or multiple 289 | domain controllers, and each domain controller normally has multiple naming 290 | contexts. 291 | 292 | 293 | 294 | def domains(self): 295 | """Return a list of all domains in the forest.""" 296 | 297 | 298 | 299 | This function returns a list of all domains that are present in the forest. 300 | 301 | 302 | 303 | def close(self): 304 | """Close any active LDAP connection.""" 305 | 306 | 307 | 308 | The close() closes all currently open connections to 309 | the Active Directory. 310 | 311 | 312 | 313 | def search(self, filter=None, base=None, scope=None, attrs=None, 314 | server=None, scheme=None): 315 | """Search the Active Directory.""" 316 | 317 | 318 | 319 | The search() method is probably the most important 320 | function in Python-AD. It searches the Active Directory and returns a list 321 | of objects that match the query. The filter argument 322 | specifies the LDAP search filter. If it is absent, the default filter is 323 | (objectClass=*). The base 324 | arguments gives the LDAP search base. This search base must either be blank 325 | ('') in which case the rootDSE is searched, or it must be 326 | within one of the naming contexts of the forest. The 327 | scope parameter must be a string of the value 328 | 'base', 'onelevel' or 329 | 'subtree'. For compatibility reasons, the 330 | SCOPE_* constants as defined by Python-LDAP are accepted 331 | as well. The attrs parameter specifies the attributes 332 | to request. If it is given, it must be a list of strings. If it is not 333 | specified, then the default is to request all attributes. The 334 | server parameter is another optional parameter that 335 | specifies the server to bind to. Normally, Python-AD operates in 336 | serverless binding mode. In this mode, a suitable 337 | domain controller is selected automatically. In some situation this may not 338 | be desidered however and for these situations the 339 | server can be used. Finally the 340 | scheme parameter specifies the search scheme. It must 341 | be one of 'ldap' (the default) or 'gc' 342 | to search the global catalog. 343 | 344 | 345 | 346 | The return value of search() is a list of 2-tuples. 347 | Each tuple consists of a distinguished name and a dictionary of attributes. 348 | The dictionary has string keys (the attribute names) and a list of strings 349 | as it values (the attribute values). 350 | 351 | 352 | 353 | def add(self, dn, attrs, server=None): 354 | """Add a new object to Active Directory.""" 355 | 356 | 357 | 358 | The add() function adds an object to the directory. The 359 | parameter dn specifies the distinguished name of the 360 | attribute to be added. The parameter attrs specifies 361 | the attributes of the object. It must be a list of 2-tuples, with the first 362 | tuple entry the attribute name, and the second tuple entry a list of strings 363 | containing the attribute values. The server parameter 364 | can be used to override the default binding behaviour and has the same 365 | meaning as for search(). 366 | 367 | 368 | 369 | def modify(self, dn, mods, server=None): 370 | """Modify an LDAP object.""" 371 | 372 | 373 | 374 | The modify() method modifies an existing object in the 375 | directory. The dn parameter specifies the 376 | distinguished name of the object modify, while mods 377 | specifies the modifications. The latter must be a list of 3-tuples. Each 378 | tuple consists of the modify operation (one of 'add', 379 | 'replace' or 'delete' to add, replace 380 | or delete an attribute value respectively -- the Python-LDAP 381 | MOD_* constants are supported for compatibility), the 382 | attribute name, and the attribute value. The latter must be a list of 383 | strings. The server parameter can be used to override 384 | the default binding behaviour and has the same meaning as for 385 | search(). 386 | 387 | 388 | 389 | def delete(self, dn, server=None): 390 | """Delete the LDAP object referenced by `dn'.""" 391 | 392 | 393 | 394 | The delete() method removes an object from the 395 | directory. The dn parameter specifies the 396 | distinguished name of the object to remove. The server 397 | parameter can be used to override the default binding behaviour and has the 398 | same meaning as for search(). 399 | 400 | 401 | 402 | def modrdn(self, dn, newrdn, delold=True, server=None): 403 | """Change the RDN of an object in Active Direcotry.""" 404 | 405 | 406 | 407 | The modrdn() method modifies the relative distinuished 408 | name of the LDAP object with distinguished name dn to 409 | the value specified by the newrdn, which must be in 410 | the form of "attr=value". The 411 | delold specifies whether the old RDN value is 412 | retained or not. The server parameter has the same 413 | meaning as for search(). 414 | 415 | 416 | 417 | def rename(self, dn, newrdn, newsuperior=None, delold=True, server=None): 418 | """Change the RDN of an object in Active Directory, and optionally 419 | move it to a new part of the tree.""" 420 | 421 | 422 | 423 | The rename() method is like 424 | modrdn() but is also allows the object to be moved to a 425 | new place in the directory by means of the 426 | newsuperior parameter. 427 | 428 | 429 | 430 | def set_password(self, principal, password, server=None): 431 | """Set the password of `principal' to `password'.""" 432 | 433 | 434 | 435 | This function sets the password for principal to 436 | password. The server parameter 437 | can again be used to override the default server binding as for the 438 | search() method. 439 | 440 | 441 | 442 | def change_password(self, principal, oldpass, newpass, server=None): 443 | """Change the password of `principal' to `password'.""" 444 | 445 | 446 | 447 | This function changes the password for principal 448 | principal. The old password must be given in 449 | oldpass and the new password in 450 | newpass. 451 | 452 | 453 |
454 | 455 |
456 | 457 | Resource Location 458 | 459 | 460 | Resource location is an problem that is normally handled transparently by 461 | Python-AD. In the default situation, domain controllers are looked up 462 | automatically by a global instance of the Locator 463 | class. It is possible however to use this class directly. This is most 464 | useful when targetting a specific domain controller with the 465 | server argument that many methods of the 466 | Client class accept. 467 | 468 | 469 | 470 | The Locator class is available from the 471 | ad package: 472 | 473 | 474 | from ad import Locator 475 | 476 | 477 | 478 | The constructor takes one optional parameter: the site. 479 | 480 | 481 | 482 | class Locator(object): 483 | """Locate domain controllers. 484 | 485 | def __init__(self, site=None): 486 | """Constructor.""" 487 | 488 | 489 | 490 | The site specifies the AD site the current system is 491 | in. Normally this value is autodetected, but in some situations you may want 492 | to override this. 493 | 494 | 495 | 496 | The Locator class defines the following methods: 497 | 498 | 499 | 500 | def locate(self, domain, role=None): 501 | """Locate one domain controller.""" 502 | 503 | 504 | 505 | The locate() method locates one domain controller for 506 | the domain domain. The optional parameter 507 | role, if given, specifies the desired role of the 508 | domain controller. This can be one of 'dc', 509 | 'gc' or 'pdc' for a normal domain 510 | controller, a global catalog, or the domain controller with the primary 511 | domain controller emulator role respectively. The default is to locate an 512 | ordinary domain controller. The return value of this method is a string 513 | containing the host name of the domain controller. If no domain controller 514 | is found, an exception is raised. 515 | 516 | 517 | 518 | def locate_many(self, domain, role=None, maxservers=None): 519 | """Locate a list of up to `maxservers' of domain controllers.""" 520 | 521 | 522 | 523 | This method locates up to maxservers domain 524 | controllers. The domain and 525 | role parameters are as for 526 | locate(). The return value is a list of strings 527 | containing the host names of the selected domain controllers. If no domain 528 | controllers are found, an empty list is returned. 529 | 530 | 531 |
532 | 533 |
534 | 535 | Error Handling 536 | 537 | Two exceptions are defined for error handling. It is recommended to import 538 | the exception as per the code fragment below: 539 | 540 | 541 | from ad import Error as ADError 542 | from ad import LDAPError 543 | 544 | 545 | The ADError exception is raised for all errors that 546 | are encountered by Python-AD. The LDAPError exception 547 | is imported from Python-LDAP and is raised when an exception is raised by 548 | that module. Therefore, to ensure that all exceptions are caught, you always 549 | need to capture both exceptions, as illustrated by the example below: 550 | 551 | 552 | client = Client(domain) 553 | try: 554 | client.add(dn, attrs) 555 | except (ADError, LDAPError): 556 | pass 557 | 558 | 559 |
560 | 561 |
562 | --------------------------------------------------------------------------------