summaryrefslogtreecommitdiff
path: root/src/tests/t_iprop.py
blob: 8e23cd5de9db3026bf6a664bf4fe869703ef8094 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
#!/usr/bin/python

import os
import re

from k5test import *

# Read lines from kpropd output until we are synchronized.  Error if
# full_expected is true and we didn't see a full propagation or vice
# versa.
def wait_for_prop(kpropd, full_expected, expected_old, expected_new):
    output('*** Waiting for sync from kpropd\n')
    full_seen = sleep_seen = False
    old_sno = new_sno = -1
    while True:
        line = kpropd.stdout.readline()
        if line == '':
            fail('kpropd process exited unexpectedly')
        output('kpropd: ' + line)

        m = re.match(r'Calling iprop_get_updates_1 \(sno=(\d+) ', line)
        if m:
            if not full_seen:
                old_sno = int(m.group(1))
            # Also record this as the new sno, in case we get back
            # UPDATE_NIL.
            new_sno = int(m.group(1))

        m = re.match(r'Got incremental updates \(sno=(\d+) ', line)
        if m:
            new_sno = int(m.group(1))

        if 'KDC is synchronized' in line or 'Incremental updates:' in line:
            break

        # After a full resync request, these lines could appear in
        # either order.
        if 'Waiting for' in line:
            sleep_seen = True
        if 'load process for full propagation completed' in line:
            full_seen = True

        # Detect some failure conditions.
        if 'Still waiting for full resync' in line:
            fail('kadmind gave consecutive full resyncs')
        if 'Rejected connection' in line:
            fail('kpropd rejected kprop connection')
        if 'get updates failed' in line:
            fail('iprop_get_updates failed')
        if 'permission denied' in line:
            fail('kadmind denied update')
        if 'error from master' in line or 'error returned from master' in line:
            fail('kadmind reported error')
        if 'invalid return' in line:
            fail('kadmind returned invalid result')

    if full_expected and not full_seen:
        fail('Expected full dump but saw only incremental')
    if full_seen and not full_expected:
        fail('Expected incremental prop but saw full dump')
    if old_sno != expected_old:
         fail('Expected old serial %d from kpropd sync' % expected_old)
    if new_sno != expected_new:
         fail('Expected new serial %d from kpropd sync' % expected_new)

    # Wait until kpropd is sleeping before continuing, to avoid races.
    # (This is imperfect since there's there is a short window between
    # the fprintf and the sleep; kpropd will need design changes to
    # fix that.)
    while True:
        line = kpropd.stdout.readline()
        output('kpropd: ' + line)
        if 'Waiting for' in line:
            break
    output('*** Sync complete\n')

# Verify the output of kproplog against the expected number of
# entries, first and last serial number, and a list of principal names
# for the update entrires.
def check_ulog(num, first, last, entries, env=None):
    out = realm.run([kproplog], env=env)
    if 'Number of entries : ' + str(num) + '\n' not in out:
        fail('Expected %d entries' % num)
    if last:
        firststr = first and str(first) or 'None'
        if 'First serial # : ' + firststr + '\n' not in out:
            fail('Expected first serial number %d' % first)
    laststr = last and str(last) or 'None'
    if 'Last serial # : ' + laststr + '\n' not in out:
        fail('Expected last serial number %d' % last)
    assert(len(entries) == num)
    ser = first - 1
    entindex = 0
    for line in out.splitlines():
        m = re.match(r'\tUpdate serial # : (\d+)$', line)
        if m:
            ser = ser + 1
            if m.group(1) != str(ser):
                fail('Expected serial number %d in update entry' % ser)
        m = re.match(r'\tUpdate principal : (.*)$', line)
        if m:
            eprinc = entries[ser - first]
            if eprinc == None:
                fail('Expected dummy update entry %d' % ser)
            elif m.group(1) != eprinc:
                fail('Expected princ %s in update entry %d' % (eprinc, ser))
        if line == '\tDummy entry':
            eprinc = entries[ser - first]
            if eprinc != None:
                fail('Expected princ %s in update entry %d' % (eprinc, ser))

# slave1 will receive updates from master, and slave2 will receive
# updates from slave1.  Because of the awkward way iprop and kprop
# port configuration currently works, we need separate config files
# for the slave and master sides of slave1, but they use the same DB
# and ulog file.
conf = {'realms': {'$realm': {'iprop_enable': 'true',
                              'iprop_logfile': '$testdir/db.ulog'}}}
conf_slave1 = {'realms': {'$realm': {'iprop_slave_poll': '600',
                                     'iprop_logfile': '$testdir/ulog.slave1'}},
               'dbmodules': {'db': {'database_name': '$testdir/db.slave1'}}}
conf_slave1m = {'realms': {'$realm': {'iprop_logfile': '$testdir/ulog.slave1',
                                      'iprop_port': '$port8'}},
               'dbmodules': {'db': {'database_name': '$testdir/db.slave1'}}}
conf_slave2 = {'realms': {'$realm': {'iprop_slave_poll': '600',
                                     'iprop_logfile': '$testdir/ulog.slave2',
                                     'iprop_port': '$port8'}},
               'dbmodules': {'db': {'database_name': '$testdir/db.slave2'}}}

conf_foo = {'libdefaults': {'default_realm': 'FOO'},
            'domain_realm': {hostname: 'FOO'}}

realm = K5Realm(kdc_conf=conf, create_user=False, start_kadmind=True)
slave1 = realm.special_env('slave1', True, kdc_conf=conf_slave1)
slave1m = realm.special_env('slave1m', True, krb5_conf=conf_foo,
                            kdc_conf=conf_slave1m)
slave2 = realm.special_env('slave2', True, kdc_conf=conf_slave2)

# A default_realm and domain_realm that do not match the KDC's realm.
# The FOO realm iprop_logfile setting is needed to run kproplog during
# a slave3 test, since kproplog has no realm option.
conf_slave3 = {'realms': {'$realm': {'iprop_slave_poll': '600',
                                     'iprop_logfile': '$testdir/ulog.slave3',
                                     'iprop_port': '$port8'},
                          'FOO': {'iprop_logfile': '$testdir/ulog.slave3'}},
               'dbmodules': {'db': {'database_name': '$testdir/db.slave3'}}}
slave3 = realm.special_env('slave3', True, krb5_conf=conf_foo,
                           kdc_conf=conf_slave3)

# A default realm and a domain realm map that differ.
krb5_conf_slave4 = {'domain_realm': {hostname: 'FOO'}}
conf_slave4 = {'realms': {'$realm': {'iprop_slave_poll': '600',
                                     'iprop_logfile': '$testdir/ulog.slave4',
                                     'iprop_port': '$port8'}},
               'dbmodules': {'db': {'database_name': '$testdir/db.slave4'}}}
slave4 = realm.special_env('slave4', True, krb5_conf=krb5_conf_slave4,
                            kdc_conf=conf_slave4)

# Define some principal names.  pr3 is long enough to cause internal
# reallocs, but not long enough to grow the basic ulog entry size.
pr1 = 'wakawaka@' + realm.realm
pr2 = 'w@' + realm.realm
c = 'chocolate-flavored-school-bus'
cs = c + '/'
pr3 = (cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + c +
       '@' + realm.realm)

# Create the kpropd ACL file.
acl_file = os.path.join(realm.testdir, 'kpropd-acl')
acl = open(acl_file, 'w')
acl.write(realm.host_princ + '\n')
acl.close()

ulog = os.path.join(realm.testdir, 'db.ulog')
if not os.path.exists(ulog):
    fail('update log not created: ' + ulog)

# Create the principal used to authenticate kpropd to kadmind.
kiprop_princ = 'kiprop/' + hostname
realm.extract_keytab(kiprop_princ, realm.keytab)

# Create the initial slave databases.
dumpfile = os.path.join(realm.testdir, 'dump')
realm.run([kdb5_util, 'dump', dumpfile])
realm.run([kdb5_util, 'load', dumpfile], slave1)
realm.run([kdb5_util, 'load', dumpfile], slave2)
realm.run([kdb5_util, '-r', realm.realm, 'load', dumpfile], slave3)
realm.run([kdb5_util, 'load', dumpfile], slave4)

# Reinitialize the master ulog so we know exactly what to expect in
# it.
realm.run([kproplog, '-R'])
check_ulog(1, 1, 1, [None])

# Make some changes to the master DB.
realm.addprinc(pr1)
realm.addprinc(pr3)
realm.addprinc(pr2)
realm.run([kadminl, 'modprinc', '-allow_tix', pr2])
realm.run([kadminl, 'modprinc', '+allow_tix', pr2])
check_ulog(6, 1, 6, [None, pr1, pr3, pr2, pr2, pr2])

# Start kpropd for slave1 and get a full dump from master.
kpropd1 = realm.start_kpropd(slave1, ['-d'])
wait_for_prop(kpropd1, True, 1, 6)
out = realm.run([kadminl, 'listprincs'], env=slave1)
if pr1 not in out or pr2 not in out or pr3 not in out:
    fail('slave1 does not have all principals from master')
check_ulog(1, 6, 6, [None], slave1)

# Make a change and check that it propagates incrementally.
realm.run([kadminl, 'modprinc', '-allow_tix', pr2])
check_ulog(7, 1, 7, [None, pr1, pr3, pr2, pr2, pr2, pr2])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 6, 7)
check_ulog(2, 6, 7, [None, pr2], slave1)
realm.run([kadminl, 'getprinc', pr2], env=slave1,
          expected_msg='Attributes: DISALLOW_ALL_TIX')

# Start kadmind -proponly for slave1.  (Use the slave1m environment
# which defines iprop_port to $port8.)
slave1_out_dump_path = os.path.join(realm.testdir, 'dump.slave1.out')
slave2_in_dump_path = os.path.join(realm.testdir, 'dump.slave2.in')
slave2_kprop_port = str(realm.portbase + 9)
realm.start_server([kadmind, '-r', realm.realm, '-nofork', '-proponly', '-W',
                    '-p', kdb5_util, '-K', kprop, '-k', slave2_kprop_port,
                    '-F', slave1_out_dump_path], 'starting...', slave1m)

# Test similar default_realm and domain_realm map settings with -r realm.
slave3_in_dump_path = os.path.join(realm.testdir, 'dump.slave3.in')
kpropd3 = realm.start_server([kpropd, '-d', '-D', '-r', realm.realm, '-P',
                              slave2_kprop_port, '-f', slave3_in_dump_path,
                              '-p', kdb5_util, '-a', acl_file, '-A', hostname],
                             'ready', slave3)
wait_for_prop(kpropd3, True, 1, 7)
out = realm.run([kadminl, '-r', realm.realm, 'listprincs'], env=slave3)
if pr1 not in out or pr2 not in out or pr3 not in out:
    fail('slave3 does not have all principals from slave1')
check_ulog(1, 7, 7, [None], env=slave3)

# Test an incremental propagation for the kpropd -r case.
realm.run([kadminl, 'modprinc', '-maxlife', '20 minutes', pr1])
check_ulog(8, 1, 8, [None, pr1, pr3, pr2, pr2, pr2, pr2, pr1])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 7, 8)
check_ulog(3, 6, 8, [None, pr2, pr1], slave1)
realm.run([kadminl, 'getprinc', pr1], env=slave1,
          expected_msg='Maximum ticket life: 0 days 00:20:00')
kpropd3.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd3, False, 7, 8)
check_ulog(2, 7, 8, [None, pr1], slave3)
realm.run([kadminl, '-r', realm.realm, 'getprinc', pr1], env=slave3,
          expected_msg='Maximum ticket life: 0 days 00:20:00')
stop_daemon(kpropd3)

# Test dissimilar default_realm and domain_realm map settings (no -r realm).
slave4_in_dump_path = os.path.join(realm.testdir, 'dump.slave4.in')
kpropd4 = realm.start_server([kpropd, '-d', '-D', '-P', slave2_kprop_port,
                              '-f', slave4_in_dump_path, '-p', kdb5_util,
                              '-a', acl_file, '-A', hostname], 'ready', slave4)
wait_for_prop(kpropd4, True, 1, 8)
out = realm.run([kadminl, 'listprincs'], env=slave4)
if pr1 not in out or pr2 not in out or pr3 not in out:
    fail('slave4 does not have all principals from slave1')
stop_daemon(kpropd4)

# Start kpropd for slave2.  The -A option isn't needed since we're
# talking to the same host as master (we specify it anyway to exercise
# the code), but slave2 defines iprop_port to $port8 so it will talk
# to slave1.  Get a full dump from slave1.
kpropd2 = realm.start_server([kpropd, '-d', '-D', '-P', slave2_kprop_port,
                              '-f', slave2_in_dump_path, '-p', kdb5_util,
                              '-a', acl_file, '-A', hostname], 'ready', slave2)
wait_for_prop(kpropd2, True, 1, 8)
check_ulog(2, 7, 8, [None, pr1], slave2)
out = realm.run([kadminl, 'listprincs'], env=slave1)
if pr1 not in out or pr2 not in out or pr3 not in out:
    fail('slave2 does not have all principals from slave1')

# Make another change and check that it propagates incrementally to
# both slaves.
realm.run([kadminl, 'modprinc', '-maxrenewlife', '22 hours', pr1])
check_ulog(9, 1, 9, [None, pr1, pr3, pr2, pr2, pr2, pr2, pr1, pr1])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 8, 9)
check_ulog(4, 6, 9, [None, pr2, pr1, pr1], slave1)
realm.run([kadminl, 'getprinc', pr1], env=slave1,
          expected_msg='Maximum renewable life: 0 days 22:00:00\n')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 8, 9)
check_ulog(3, 7, 9, [None, pr1, pr1], slave2)
realm.run([kadminl, 'getprinc', pr1], env=slave2,
          expected_msg='Maximum renewable life: 0 days 22:00:00\n')

# Reset the ulog on slave1 to force a full resync from master.  The
# resync will use the old dump file and then propagate changes.
# slave2 should still be in sync with slave1 after the resync, so make
# sure it doesn't take a full resync.
realm.run([kproplog, '-R'], slave1)
check_ulog(1, 1, 1, [None], slave1)
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, True, 1, 9)
check_ulog(4, 6, 9, [None, pr2, pr1, pr1], slave1)
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 9, 9)
check_ulog(3, 7, 9, [None, pr1, pr1], slave2)

# Make another change and check that it propagates incrementally to
# both slaves.
realm.run([kadminl, 'modprinc', '+allow_tix', pr2])
check_ulog(10, 1, 10, [None, pr1, pr3, pr2, pr2, pr2, pr2, pr1, pr1, pr2])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 9, 10)
check_ulog(5, 6, 10, [None, pr2, pr1, pr1, pr2], slave1)
realm.run([kadminl, 'getprinc', pr2], env=slave1, expected_msg='Attributes:\n')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 9, 10)
check_ulog(4, 7, 10, [None, pr1, pr1, pr2], slave2)
realm.run([kadminl, 'getprinc', pr2], env=slave2, expected_msg='Attributes:\n')

# Create a policy and check that it propagates via full resync.
realm.run([kadminl, 'addpol', '-minclasses', '2', 'testpol'])
check_ulog(1, 1, 1, [None])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, True, 10, 1)
check_ulog(1, 1, 1, [None], slave1)
realm.run([kadminl, 'getpol', 'testpol'], env=slave1,
          expected_msg='Minimum number of password character classes: 2')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, True, 10, 1)
check_ulog(1, 1, 1, [None], slave2)
realm.run([kadminl, 'getpol', 'testpol'], env=slave2,
          expected_msg='Minimum number of password character classes: 2')

# Modify the policy and test that it also propagates via full resync.
realm.run([kadminl, 'modpol', '-minlength', '17', 'testpol'])
check_ulog(1, 1, 1, [None])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, True, 1, 1)
check_ulog(1, 1, 1, [None], slave1)
realm.run([kadminl, 'getpol', 'testpol'], env=slave1,
          expected_msg='Minimum password length: 17')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, True, 1, 1)
check_ulog(1, 1, 1, [None], slave2)
realm.run([kadminl, 'getpol', 'testpol'], env=slave2,
          expected_msg='Minimum password length: 17')

# Delete the policy and test that it propagates via full resync.
realm.run([kadminl, 'delpol', 'testpol'])
check_ulog(1, 1, 1, [None])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, True, 1, 1)
check_ulog(1, 1, 1, [None], slave1)
realm.run([kadminl, 'getpol', 'testpol'], env=slave1, expected_code=1,
          expected_msg='Policy does not exist')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, True, 1, 1)
check_ulog(1, 1, 1, [None], slave2)
realm.run([kadminl, 'getpol', 'testpol'], env=slave2, expected_code=1,
          expected_msg='Policy does not exist')

# Modify a principal on the master and test that it propagates incrementally.
realm.run([kadminl, 'modprinc', '-maxlife', '10 minutes', pr1])
check_ulog(2, 1, 2, [None, pr1])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 1, 2)
check_ulog(2, 1, 2, [None, pr1], slave1)
realm.run([kadminl, 'getprinc', pr1], env=slave1,
          expected_msg='Maximum ticket life: 0 days 00:10:00')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 1, 2)
check_ulog(2, 1, 2, [None, pr1], slave2)
realm.run([kadminl, 'getprinc', pr1], env=slave2,
          expected_msg='Maximum ticket life: 0 days 00:10:00')

# Delete a principal and test that it propagates incrementally.
realm.run([kadminl, 'delprinc', pr3])
check_ulog(3, 1, 3, [None, pr1, pr3])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 2, 3)
check_ulog(3, 1, 3, [None, pr1, pr3], slave1)
realm.run([kadminl, 'getprinc', pr3], env=slave1, expected_code=1,
          expected_msg='Principal does not exist')
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 2, 3)
check_ulog(3, 1, 3, [None, pr1, pr3], slave2)
realm.run([kadminl, 'getprinc', pr3], env=slave2, expected_code=1,
          expected_msg='Principal does not exist')

# Rename a principal and test that it propagates incrementally.
renpr = "quacked@" + realm.realm
realm.run([kadminl, 'renprinc', pr1, renpr])
check_ulog(6, 1, 6, [None, pr1, pr3, renpr, pr1, renpr])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, False, 3, 6)
check_ulog(6, 1, 6, [None, pr1, pr3, renpr, pr1, renpr], slave1)
realm.run([kadminl, 'getprinc', pr1], env=slave1, expected_code=1,
          expected_msg='Principal does not exist')
realm.run([kadminl, 'getprinc', renpr], env=slave1)
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, False, 3, 6)
check_ulog(6, 1, 6, [None, pr1, pr3, renpr, pr1, renpr], slave2)
realm.run([kadminl, 'getprinc', pr1], env=slave2, expected_code=1,
          expected_msg='Principal does not exist')
realm.run([kadminl, 'getprinc', renpr], env=slave2)

pr1 = renpr

# Reset the ulog on the master to force a full resync.
realm.run([kproplog, '-R'])
check_ulog(1, 1, 1, [None])
kpropd1.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd1, True, 6, 1)
check_ulog(1, 1, 1, [None], slave1)
kpropd2.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd2, True, 6, 1)
check_ulog(1, 1, 1, [None], slave2)

# Stop the kprop daemons so we can test kpropd -t.
stop_daemon(kpropd1)
stop_daemon(kpropd2)

# Test the case where no updates are needed.
out = realm.run_kpropd_once(slave1, ['-d'])
if 'KDC is synchronized' not in out:
    fail('Expected synchronized from kpropd -t')
check_ulog(1, 1, 1, [None], slave1)

# Make a change on the master and fetch it incrementally.
realm.run([kadminl, 'modprinc', '-maxlife', '5 minutes', pr1])
check_ulog(2, 1, 2, [None, pr1])
out = realm.run_kpropd_once(slave1, ['-d'])
if 'Got incremental updates (sno=2 ' not in out:
    fail('Expected full dump and synchronized from kpropd -t')
check_ulog(2, 1, 2, [None, pr1], slave1)
realm.run([kadminl, 'getprinc', pr1], env=slave1,
          expected_msg='Maximum ticket life: 0 days 00:05:00')

# Propagate a policy change via full resync.
realm.run([kadminl, 'addpol', '-minclasses', '3', 'testpol'])
check_ulog(1, 1, 1, [None])
out = realm.run_kpropd_once(slave1, ['-d'])
if ('Full propagation transfer finished' not in out or
    'KDC is synchronized' not in out):
    fail('Expected full dump and synchronized from kpropd -t')
check_ulog(1, 1, 1, [None], slave1)
realm.run([kadminl, 'getpol', 'testpol'], env=slave1,
          expected_msg='Minimum number of password character classes: 3')

success('iprop tests')