2 Kerberos Backend Interface
4 This module is intended to be a thin wrapper around Kerberos operations.
5 Methods on the connection object correspond in a straightforward way to
6 calls to the Kerberos Master server.
8 A Kerberos principal is the second half of a CSC UNIX account. The principal
9 stores the user's password and and is used for all authentication on CSC
10 systems. Accounts that do not authenticate (e.g. club accounts) do not need
13 Unfortunately, there are no Python bindings to libkadm at this time. As a
14 temporary workaround, this module communicates with the kadmin CLI interface
15 via a pseudo-terminal and a pipe.
21 class KrbException(Exception):
22 """Exception class for all Kerberos-related errors."""
26 class KrbConnection(object):
28 Connection to the Kerberos master server (kadmind). All Kerberos
29 principal updates are made via this class.
31 Exceptions: (all methods)
32 KrbException - on query/update failure
35 connection = KrbConnection()
36 connection.connect(...)
38 # make queries and updates, e.g.
39 connection.delete_principal("mspang")
41 connection.disconnect()
48 def connect(self, principal, keytab):
50 Establishes the connection to the Kerberos master server.
53 principal - the Kerberos princiapl to authenticate as
54 keytab - keytab filename for authentication
56 Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab')
60 if not os.access(keytab, os.R_OK):
61 raise KrbException("cannot access Kerberos keytab: %s" % keytab)
64 kadmin = '/usr/sbin/kadmin'
65 kadmin_args = ['kadmin', '-p', principal, '-kt', keytab]
67 # fork the kadmin command
68 self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args)
70 # read welcome messages
71 welcome = self.read_result()
73 # sanity checks on welcome messages
77 if line.find("Authenticating") == 0:
80 # ignore log file message
81 elif line.find("kadmin.log") != -1:
86 raise KrbException("unexpected kadmin output: " + welcome[0])
90 """Close the connection to the master server."""
94 # close the pipe connected to kadmin's standard input
97 # close the master pty connected to kadmin's stdout
103 # wait for kadmin to terminate
104 os.waitpid(self.pid, 0)
109 """Determine whether the connection has been established."""
111 return self.pid is not None
115 ### Helper Methods ###
117 def read_result(self):
119 Helper function to read output of kadmin until it
122 Returns: a list of lines returned by kadmin
125 # list of lines output by kadmin
129 # the kadmin prompt that signals the end output
130 # note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
133 # timeout variables. the timeout will start at timeout and
134 # increase up to max_timeout when read() returns nothing (i.e., times out)
136 timeout_increment = 0.10
137 timeout_maximum = 1.00
139 # input loop: read from kadmin until the kadmin prompt
143 # attempt to read any available data
144 data = self.kadm_out.read(block=False, timeout=timeout)
150 # so wait longer for data next time
151 if timeout < timeout_maximum:
152 timeout += timeout_increment
155 # give up after too much waiting
158 # check kadmin status
159 status = os.waitpid(self.pid, os.WNOHANG)
163 raise KrbException("timeout while reading response from kadmin")
168 raise KrbException("kadmin died while reading response:\n%s\n%s" % ("\n".join(lines), buf))
170 # break into lines and save all but the final
171 # line (which is incomplete) into result
172 lines = buf.split("\n")
179 # if the incomplete line in the buffer is the kadmin prompt,
180 # then the result is complete and may be returned
181 if buf.strip() == prompt:
187 def execute(self, command):
189 Helper function to execute a kadmin command.
192 command - command string to pass on to kadmin
194 Returns: a list of lines output by the command
197 # there should be no remaining output from the previous
198 # command. if there is then something is broken.
199 stale_output = self.kadm_out.read(block=False, timeout=0)
200 if stale_output != '':
201 raise KrbException("unexpected kadmin output: " + stale_output)
203 # send the command to kadmin
204 self.kadm_in.write(command + "\n")
207 # read the command output and return it
208 result = self.read_result()
215 def list_principals(self):
217 Retrieve a list of Kerberos principals.
219 Returns: a list of principals
221 Example: connection.list_principals() -> [
222 "ceo/admin@CSCLUB.UWATERLOO.CA",
223 "sysadmin/admin@CSCLUB.UWATERLOO.CA",
224 "mspang@CSCLUB.UWATERLOO.CA",
229 principals = self.execute("list_principals")
231 # assuming that there at least some host principals
232 if len(principals) < 1:
233 raise KrbException("no kerberos principals")
235 # detect error message
236 if principals[0].find("kadmin:") == 0:
237 raise KrbException("list_principals returned error: " + principals[0])
239 # verify principals are well-formed
240 for principal in principals:
241 if principal.find("@") == -1:
242 raise KrbException('malformed pricipal: "' + principal + '"')
247 def get_principal(self, principal):
249 Retrieve principal details.
251 Returns: a dictionary of principal attributes
253 Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> {
254 "Principal": "ceo/admin@CSCLUB.UWATERLOO.CA",
260 output = self.execute('get_principal "' + principal + '"')
262 # detect error message
263 if output[0].find("kadmin:") == 0:
264 raise KrbException("get_principal returned error: " + output[0])
267 if output[0].find("get_principal: ") == 0:
269 message = output[0][15:]
271 # principal does not exist => None
272 if message.find("Principal does not exist") == 0:
275 # dictionary to store attributes
276 principal_attributes = {}
278 # attributes that will not be returned
279 ignore_attributes = ['Key']
281 # split output into a dictionary of attributes
283 key, value = line.split(":", 1)
284 value = value.strip()
285 if not key in ignore_attributes:
286 principal_attributes[key] = value
288 return principal_attributes
293 Retrieve privileges of the current principal.
295 Returns: a list of privileges
297 Example: connection.get_privs() ->
298 [ "GET", "ADD", "MODIFY", "DELETE" ]
301 output = self.execute("get_privs")
303 # one line of output is expected
305 raise KrbException("unexpected output of get_privs: " + output[1])
307 # detect error message
308 if output[0].find("kadmin:") == 0:
309 raise KrbException("get_privs returned error: " + output[0])
311 # parse output by removing the prefix and splitting it around spaces
312 if output[0][:20] != "current privileges: ":
313 raise KrbException("malformed get_privs output: " + output[0])
314 privs = output[0][20:].split(" ")
319 def add_principal(self, principal, password):
321 Create a new principal.
324 principal - the name of the principal
325 password - the principal's initial password
327 Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
330 # exec the add_principal command
331 if password.find('"') == -1:
332 self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n')
334 # fools at MIT didn't bother implementing escaping, so passwords
335 # that contain double quotes must be treated specially
337 self.kadm_in.write('add_principal "' + principal + '"\n')
338 self.kadm_in.write(password + "\n" + password + "\n")
340 # send request and read response
342 output = self.read_result()
348 # ignore NOTICE lines
349 if line.find("NOTICE:") == 0:
353 elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
356 # record whether success message was encountered
357 elif line.find("Principal") == 0 and line.find("created.") != 0:
361 elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0:
364 if line.find("already exists") != -1:
365 raise KrbException("principal already exists")
369 raise KrbException(line)
373 raise KrbException("unexpected add_principal output: " + line)
375 # ensure success message was received
377 raise KrbException("kadmin did not acknowledge principal creation")
380 def delete_principal(self, principal):
384 Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
387 # exec the delete_principal command and read response
388 self.kadm_in.write('delete_principal -force "' + principal + '"\n')
390 output = self.read_result()
397 if line.find("Make sure that") == 0:
400 # record whether success message was encountered
401 elif line.find("Principal") == 0 and line.find("deleted.") != -1:
405 elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0:
408 if line.find("does not exist") != -1:
409 raise KrbException("principal does not exist")
413 raise KrbException(line)
417 raise KrbException("unexpected delete_principal output: " + line)
419 # ensure success message was received
421 raise KrbException("did not receive principal deleted")
424 def change_password(self, principal, password):
426 Changes a principal's password.
428 Example: connection.change_password("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
431 # exec the add_principal command
432 if password.find('"') == -1:
433 self.kadm_in.write('change_password -pw "' + password + '" "' + principal + '"\n')
435 self.kadm_in.write('change_password "' + principal + '"\n')
436 self.kadm_in.write(password + "\n" + password + "\n")
438 # send request and read response
440 output = self.read_result()
446 # ignore NOTICE lines
447 if line.find("NOTICE:") == 0:
451 elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
454 # record whether success message was encountered
455 elif line.find("Password") == 0 and line.find("changed.") != 0:
459 elif line.find("change_password:") == 0 or line.find("kadmin:") == 0:
460 raise KrbException(line)
464 raise KrbException("unexpected change_password output: " + line)
466 # ensure success message was received
468 raise KrbException("kadmin did not acknowledge password change")
474 if __name__ == '__main__':
476 from csc.common.test import *
479 conffile = '/etc/csc/kerberos.cf'
481 cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
482 principal = cfg['admin_principal'][1:-1]
483 keytab = cfg['admin_keytab'][1:-1]
484 realm = cfg['realm'][1:-1]
486 # t=test p=principal e=expected
487 tpname = 'testpirate' + '@' + realm
488 tpw = str(random.randint(10**30, 10**31-1)) + 'YAR!'
489 eprivs = ['GET', 'ADD', 'MODIFY', 'DELETE']
492 connection = KrbConnection()
495 test(connection.connect)
496 connection.connect(principal, keytab)
500 connection.delete_principal(tpname)
504 test(connection.connected)
505 assert_equal(True, connection.connected())
508 test(connection.add_principal)
509 connection.add_principal(tpname, tpw)
512 test(connection.list_principals)
513 pals = connection.list_principals()
514 assert_equal(True, tpname in pals)
517 test(connection.get_privs)
518 privs = connection.get_privs()
519 assert_equal(eprivs, privs)
522 test(connection.get_principal)
523 princ = connection.get_principal(tpname)
524 assert_equal(tpname, princ['Principal'])
527 test(connection.delete_principal)
528 connection.delete_principal(tpname)
529 assert_equal(None, connection.get_principal(tpname))
532 test(connection.disconnect)
533 connection.disconnect()
534 assert_equal(False, connection.connected())