Skip to content

Commit 7dc1e62

Browse files
authored
SlapdObject directory based configuration method, and slapadd implementation (#382)
#382
1 parent ca684f8 commit 7dc1e62

File tree

4 files changed

+156
-85
lines changed

4 files changed

+156
-85
lines changed

Doc/spelling_wordlist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ serverctrls
129129
sessionSourceIp
130130
sessionSourceName
131131
sessionTrackingIdentifier
132+
slapadd
132133
sizelimit
133134
slapd
135+
startup
134136
stderr
135137
stdout
136138
str

Lib/slapdtest/_slapdtest.py

Lines changed: 68 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,33 @@
2323

2424
HERE = os.path.abspath(os.path.dirname(__file__))
2525

26-
# a template string for generating simple slapd.conf file
27-
SLAPD_CONF_TEMPLATE = r"""
28-
serverID %(serverid)s
29-
moduleload back_%(database)s
30-
%(include_directives)s
31-
loglevel %(loglevel)s
32-
allow bind_v2
33-
34-
authz-regexp
35-
"gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth"
36-
"%(rootdn)s"
37-
38-
database %(database)s
39-
directory "%(directory)s"
40-
suffix "%(suffix)s"
41-
rootdn "%(rootdn)s"
42-
rootpw "%(rootpw)s"
43-
44-
TLSCACertificateFile "%(cafile)s"
45-
TLSCertificateFile "%(servercert)s"
46-
TLSCertificateKeyFile "%(serverkey)s"
47-
# ignore missing client cert but fail with invalid client cert
48-
TLSVerifyClient try
49-
50-
authz-regexp
51-
"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)"
52-
"ldap://ou=people,dc=local???($1)"
53-
26+
# a template string for generating simple slapd.d file
27+
SLAPD_CONF_TEMPLATE = r"""dn: cn=config
28+
objectClass: olcGlobal
29+
cn: config
30+
olcServerID: %(serverid)s
31+
olcLogLevel: %(loglevel)s
32+
olcAllows: bind_v2
33+
olcAuthzRegexp: {0}"gidnumber=%(root_gid)s\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" "%(rootdn)s"
34+
olcAuthzRegexp: {1}"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" "ldap://ou=people,dc=local???($1)"
35+
olcTLSCACertificateFile: %(cafile)s
36+
olcTLSCertificateFile: %(servercert)s
37+
olcTLSCertificateKeyFile: %(serverkey)s
38+
olcTLSVerifyClient: try
39+
40+
dn: cn=module,cn=config
41+
objectClass: olcModuleList
42+
cn: module
43+
olcModuleLoad: back_%(database)s
44+
45+
dn: olcDatabase=%(database)s,cn=config
46+
objectClass: olcDatabaseConfig
47+
objectClass: olcMdbConfig
48+
olcDatabase: %(database)s
49+
olcSuffix: %(suffix)s
50+
olcRootDN: %(rootdn)s
51+
olcRootPW: %(rootpw)s
52+
olcDbDirectory: %(directory)s
5453
"""
5554

5655
LOCALHOST = '127.0.0.1'
@@ -175,6 +174,9 @@ class SlapdObject(object):
175174
manager, the slapd server is shut down and the temporary data store is
176175
removed.
177176
177+
:param openldap_schema_files: A list of schema names or schema paths to
178+
load at startup. By default this only contains `core`.
179+
178180
.. versionchanged:: 3.1
179181
180182
Added context manager functionality
@@ -187,10 +189,10 @@ class SlapdObject(object):
187189
slapd_loglevel = 'stats stats2'
188190
local_host = LOCALHOST
189191
testrunsubdirs = (
190-
'schema',
192+
'slapd.d',
191193
)
192194
openldap_schema_files = (
193-
'core.schema',
195+
'core.ldif',
194196
)
195197

196198
TMPDIR = os.environ.get('TMP', os.getcwd())
@@ -217,8 +219,7 @@ def __init__(self):
217219
self._port = self._avail_tcp_port()
218220
self.server_id = self._port % 4096
219221
self.testrundir = os.path.join(self.TMPDIR, 'python-ldap-test-%d' % self._port)
220-
self._schema_prefix = os.path.join(self.testrundir, 'schema')
221-
self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf')
222+
self._slapd_conf = os.path.join(self.testrundir, 'slapd.d')
222223
self._db_directory = os.path.join(self.testrundir, "openldap-data")
223224
self.ldap_uri = "ldap://%s:%d/" % (self.local_host, self._port)
224225
if HAVE_LDAPI:
@@ -262,6 +263,7 @@ def _find_commands(self):
262263
self.PATH_LDAPDELETE = self._find_command('ldapdelete')
263264
self.PATH_LDAPMODIFY = self._find_command('ldapmodify')
264265
self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami')
266+
self.PATH_SLAPADD = self._find_command('slapadd')
265267

266268
self.PATH_SLAPD = os.environ.get('SLAPD', None)
267269
if not self.PATH_SLAPD:
@@ -292,7 +294,6 @@ def setup_rundir(self):
292294
os.mkdir(self.testrundir)
293295
os.mkdir(self._db_directory)
294296
self._create_sub_dirs(self.testrunsubdirs)
295-
self._ln_schema_files(self.openldap_schema_files, self.SCHEMADIR)
296297

297298
def _cleanup_rundir(self):
298299
"""
@@ -337,17 +338,8 @@ def gen_config(self):
337338
for generating specific static configuration files you have to
338339
override this method
339340
"""
340-
include_directives = '\n'.join(
341-
'include "{schema_prefix}/{schema_file}"'.format(
342-
schema_prefix=self._schema_prefix,
343-
schema_file=schema_file,
344-
)
345-
for schema_file in self.openldap_schema_files
346-
)
347341
config_dict = {
348342
'serverid': hex(self.server_id),
349-
'schema_prefix':self._schema_prefix,
350-
'include_directives': include_directives,
351343
'loglevel': self.slapd_loglevel,
352344
'database': self.database,
353345
'directory': self._db_directory,
@@ -371,29 +363,28 @@ def _create_sub_dirs(self, dir_names):
371363
self._log.debug('Create directory %s', dir_name)
372364
os.mkdir(dir_name)
373365

374-
def _ln_schema_files(self, file_names, source_dir):
375-
"""
376-
write symbolic links to original schema files
377-
"""
378-
for fname in file_names:
379-
ln_source = os.path.join(source_dir, fname)
380-
ln_target = os.path.join(self._schema_prefix, fname)
381-
self._log.debug('Create symlink %s -> %s', ln_source, ln_target)
382-
os.symlink(ln_source, ln_target)
383-
384366
def _write_config(self):
385-
"""Writes the slapd.conf file out, and returns the path to it."""
386-
self._log.debug('Writing config to %s', self._slapd_conf)
387-
with open(self._slapd_conf, 'w') as config_file:
388-
config_file.write(self.gen_config())
389-
self._log.info('Wrote config to %s', self._slapd_conf)
367+
"""Loads the slapd.d configuration."""
368+
self._log.debug("importing configuration: %s", self._slapd_conf)
369+
370+
self.slapadd(self.gen_config(), ["-n0"])
371+
ldif_paths = [
372+
schema
373+
if os.path.exists(schema)
374+
else os.path.join(self.SCHEMADIR, schema)
375+
for schema in self.openldap_schema_files
376+
]
377+
for ldif_path in ldif_paths:
378+
self.slapadd(None, ["-n0", "-l", ldif_path])
379+
380+
self._log.debug("import ok: %s", self._slapd_conf)
390381

391382
def _test_config(self):
392383
self._log.debug('testing config %s', self._slapd_conf)
393384
popen_list = [
394385
self.PATH_SLAPD,
395386
"-Ttest",
396-
"-f", self._slapd_conf,
387+
"-F", self._slapd_conf,
397388
"-u",
398389
"-v",
399390
"-d", "config"
@@ -417,8 +408,7 @@ def _start_slapd(self):
417408
urls.append(self.ldapi_uri)
418409
slapd_args = [
419410
self.PATH_SLAPD,
420-
'-f', self._slapd_conf,
421-
'-F', self.testrundir,
411+
'-F', self._slapd_conf,
422412
'-h', ' '.join(urls),
423413
]
424414
if self._log.isEnabledFor(logging.DEBUG):
@@ -523,10 +513,14 @@ def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None,
523513
stdin_data=None): # pragma: no cover
524514
if ldap_uri is None:
525515
ldap_uri = self.default_ldap_uri
526-
args = [
527-
ldapcommand,
528-
'-H', ldap_uri,
529-
] + self._cli_auth_args() + (extra_args or [])
516+
517+
if ldapcommand.split("/")[-1].startswith("ldap"):
518+
args = [ldapcommand, '-H', ldap_uri] + self._cli_auth_args()
519+
else:
520+
args = [ldapcommand, '-F', self._slapd_conf]
521+
522+
args += (extra_args or [])
523+
530524
self._log.debug('Run command: %r', ' '.join(args))
531525
proc = subprocess.Popen(
532526
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -577,6 +571,16 @@ def ldapdelete(self, dn, recursive=False, extra_args=None):
577571
extra_args.append(dn)
578572
self._cli_popen(self.PATH_LDAPDELETE, extra_args=extra_args)
579573

574+
def slapadd(self, ldif, extra_args=None):
575+
"""
576+
Runs slapadd on this slapd instance, passing it the ldif content
577+
"""
578+
self._cli_popen(
579+
self.PATH_SLAPADD,
580+
stdin_data=ldif.encode("utf-8") if ldif else None,
581+
extra_args=extra_args,
582+
)
583+
580584
def __enter__(self):
581585
self.start()
582586
return self

Tests/t_ldap_syncrepl.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,44 @@
1919
from slapdtest import SlapdObject, SlapdTestCase
2020

2121
# a template string for generating simple slapd.conf file
22-
SLAPD_CONF_PROVIDER_TEMPLATE = r"""
23-
serverID %(serverid)s
24-
moduleload back_%(database)s
25-
moduleload syncprov
26-
include "%(schema_prefix)s/core.schema"
27-
loglevel %(loglevel)s
28-
allow bind_v2
29-
30-
authz-regexp
31-
"gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth"
32-
"%(rootdn)s"
33-
34-
database %(database)s
35-
directory "%(directory)s"
36-
suffix "%(suffix)s"
37-
rootdn "%(rootdn)s"
38-
rootpw "%(rootpw)s"
39-
overlay syncprov
40-
syncprov-checkpoint 100 10
41-
syncprov-sessionlog 100
42-
index objectclass,entryCSN,entryUUID eq
22+
SLAPD_CONF_PROVIDER_TEMPLATE = r"""dn: cn=config
23+
objectClass: olcGlobal
24+
cn: config
25+
olcServerID: %(serverid)s
26+
olcLogLevel: %(loglevel)s
27+
olcAllows: bind_v2
28+
olcAuthzRegexp: {0}"gidnumber=%(root_gid)s\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" "%(rootdn)s"
29+
olcAuthzRegexp: {1}"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)" "ldap://ou=people,dc=local???($1)"
30+
olcTLSCACertificateFile: %(cafile)s
31+
olcTLSCertificateFile: %(servercert)s
32+
olcTLSCertificateKeyFile: %(serverkey)s
33+
olcTLSVerifyClient: try
34+
35+
dn: cn=module,cn=config
36+
objectClass: olcModuleList
37+
cn: module
38+
olcModuleLoad: back_%(database)s
39+
olcModuleLoad: syncprov
40+
41+
dn: olcDatabase=%(database)s,cn=config
42+
objectClass: olcDatabaseConfig
43+
objectClass: olcMdbConfig
44+
olcDatabase: %(database)s
45+
olcSuffix: %(suffix)s
46+
olcRootDN: %(rootdn)s
47+
olcRootPW: %(rootpw)s
48+
olcDbDirectory: %(directory)s
49+
olcDbIndex: objectclass,entryCSN,entryUUID eq
50+
51+
dn: olcOverlay=syncprov,olcDatabase={1}%(database)s,cn=config
52+
objectClass: olcOverlayConfig
53+
objectClass: olcSyncProvConfig
54+
olcOverlay: syncprov
55+
olcSpCheckpoint: 100 10
56+
olcSpSessionlog: 100
57+
"""
58+
59+
OTHER_CONF = r"""
4360
"""
4461

4562
# Define initial data load, both as an LDIF and as a dictionary.

Tests/t_ldapobject.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@
6262
6363
"""
6464

65+
SCHEMA_TEMPLATE = """dn: cn=mySchema,cn=schema,cn=config
66+
objectClass: olcSchemaConfig
67+
cn: mySchema
68+
olcAttributeTypes: ( 1.3.6.1.4.1.56207.1.1.1 NAME 'myAttribute'
69+
DESC 'fobar attribute'
70+
EQUALITY caseExactMatch
71+
ORDERING caseExactOrderingMatch
72+
SUBSTR caseExactSubstringsMatch
73+
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
74+
SINGLE-VALUE
75+
USAGE userApplications
76+
X-ORIGIN 'foobar' )
77+
olcObjectClasses: ( 1.3.6.1.4.1.56207.1.2.2 NAME 'myClass'
78+
DESC 'foobar objectclass'
79+
SUP top
80+
STRUCTURAL
81+
MUST myAttribute
82+
X-ORIGIN 'foobar' )"""
83+
6584

6685
class Test00_SimpleLDAPObject(SlapdTestCase):
6786
"""
@@ -94,6 +113,14 @@ def setUp(self):
94113
def tearDown(self):
95114
del self._ldap_conn
96115

116+
def reset_connection(self):
117+
try:
118+
del self._ldap_conn
119+
except AttributeError:
120+
pass
121+
122+
self._ldap_conn = self._open_ldap_conn(bytes_mode=False)
123+
97124
def test_reject_bytes_base(self):
98125
base = self.server.suffix
99126
l = self._ldap_conn
@@ -465,6 +492,22 @@ def test_passwd_s(self):
465492

466493
l.delete_s(dn)
467494

495+
def test_slapadd(self):
496+
with self.assertRaises(ldap.INVALID_DN_SYNTAX):
497+
self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [
498+
("objectClass", b'myClass'),
499+
("myAttribute", b'foobar'),
500+
])
501+
502+
self.server.slapadd(SCHEMA_TEMPLATE, ["-n0"])
503+
self.server.restart()
504+
self.reset_connection()
505+
506+
self._ldap_conn.add_s("myAttribute=foobar,ou=Container,%s" % self.server.suffix, [
507+
("objectClass", b'myClass'),
508+
("myAttribute", b'foobar'),
509+
])
510+
468511

469512
class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
470513
"""
@@ -561,6 +604,11 @@ def tearDown(self):
561604
del self._sock
562605
super(Test03_SimpleLDAPObjectWithFileno, self).tearDown()
563606

607+
def reset_connection(self):
608+
self._sock.close()
609+
del self._sock
610+
super(Test03_SimpleLDAPObjectWithFileno, self).reset_connection()
611+
564612

565613
if __name__ == '__main__':
566614
unittest.main()

0 commit comments

Comments
 (0)