""" Kerberos Backend Interface This module is intended to be a thin wrapper around Kerberos operations. Methods on the connection object correspond in a straightforward way to calls to the Kerberos Master server. A Kerberos principal is the second half of a CSC UNIX account. The principal stores the user's password and and is used for all authentication on CSC systems. Accounts that do not authenticate (e.g. club accounts) do not need a Kerberos principal. Unfortunately, there are no Python bindings to libkadm at this time. As a temporary workaround, this module communicates with the kadmin CLI interface via a pseudo-terminal and a pipe. """ import os import ipc class KrbException(Exception): """Exception class for all Kerberos-related errors.""" pass class KrbConnection(object): """ Connection to the Kerberos master server (kadmind). All Kerberos principal updates are made via this class. Exceptions: (all methods) KrbException - on query/update failure Example: connection = KrbConnection() connection.connect(...) # make queries and updates, e.g. connection.delete_principal("mspang") connection.disconnect() """ def __init__(self): self.pid = None def connect(self, principal, keytab): """ Establishes the connection to the Kerberos master server. Parameters: principal - the Kerberos princiapl to authenticate as keytab - keytab filename for authentication Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab') """ # check keytab if not os.access(keytab, os.R_OK): raise KrbException("cannot access Kerberos keytab: %s" % keytab) # command to run kadmin = '/usr/sbin/kadmin' kadmin_args = ['kadmin', '-p', principal, '-kt', keytab] # fork the kadmin command self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args) # read welcome messages welcome = self.read_result() # sanity checks on welcome messages for line in welcome: # ignore auth message if line.find("Authenticating") == 0: continue # ignore log file message elif line.find("kadmin.log") != -1: continue # error message? else: raise KrbException("unexpected kadmin output: " + welcome[0]) def disconnect(self): """Close the connection to the master server.""" if self.pid: # close the pipe connected to kadmin's standard input self.kadm_in.close() # close the master pty connected to kadmin's stdout try: self.kadm_out.close() except OSError: pass # wait for kadmin to terminate os.waitpid(self.pid, 0) self.pid = None def connected(self): """Determine whether the connection has been established.""" return self.pid is not None ### Helper Methods ### def read_result(self): """ Helper function to read output of kadmin until it prompts for input. Returns: a list of lines returned by kadmin """ # list of lines output by kadmin result = [] lines = [] # the kadmin prompt that signals the end output # note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ prompt = "kadmin:" # timeout variables. the timeout will start at timeout and # increase up to max_timeout when read() returns nothing (i.e., times out) timeout = 0.01 timeout_increment = 0.10 timeout_maximum = 1.00 # input loop: read from kadmin until the kadmin prompt buf = '' while True: # attempt to read any available data data = self.kadm_out.read(block=False, timeout=timeout) buf += data # nothing was read if data == '': # so wait longer for data next time if timeout < timeout_maximum: timeout += timeout_increment continue # give up after too much waiting else: # check kadmin status status = os.waitpid(self.pid, os.WNOHANG) if status[0] == 0: # kadmin still alive raise KrbException("timeout while reading response from kadmin") else: # kadmin died! raise KrbException("kadmin died while reading response:\n%s\n%s" % ("\n".join(lines), buf)) # break into lines and save all but the final # line (which is incomplete) into result lines = buf.split("\n") buf = lines[-1] lines = lines[:-1] for line in lines: line = line.strip() result.append(line) # if the incomplete line in the buffer is the kadmin prompt, # then the result is complete and may be returned if buf.strip() == prompt: break return result def execute(self, command): """ Helper function to execute a kadmin command. Parameters: command - command string to pass on to kadmin Returns: a list of lines output by the command """ # there should be no remaining output from the previous # command. if there is then something is broken. stale_output = self.kadm_out.read(block=False, timeout=0) if stale_output != '': raise KrbException("unexpected kadmin output: " + stale_output) # send the command to kadmin self.kadm_in.write(command + "\n") self.kadm_in.flush() # read the command output and return it result = self.read_result() return result ### Commands ### def list_principals(self): """ Retrieve a list of Kerberos principals. Returns: a list of principals Example: connection.list_principals() -> [ "ceo/admin@CSCLUB.UWATERLOO.CA", "sysadmin/admin@CSCLUB.UWATERLOO.CA", "mspang@CSCLUB.UWATERLOO.CA", ... ] """ principals = self.execute("list_principals") # assuming that there at least some host principals if len(principals) < 1: raise KrbException("no kerberos principals") # detect error message if principals[0].find("kadmin:") == 0: raise KrbException("list_principals returned error: " + principals[0]) # verify principals are well-formed for principal in principals: if principal.find("@") == -1: raise KrbException('malformed pricipal: "' + principal + '"') return principals def get_principal(self, principal): """ Retrieve principal details. Returns: a dictionary of principal attributes Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> { "Principal": "ceo/admin@CSCLUB.UWATERLOO.CA", "Policy": "[none]", ... } """ output = self.execute('get_principal "' + principal + '"') # detect error message if output[0].find("kadmin:") == 0: raise KrbException("get_principal returned error: " + output[0]) # detect more errors if output[0].find("get_principal: ") == 0: message = output[0][15:] # principal does not exist => None if message.find("Principal does not exist") == 0: return None # dictionary to store attributes principal_attributes = {} # attributes that will not be returned ignore_attributes = ['Key'] # split output into a dictionary of attributes for line in output: key, value = line.split(":", 1) value = value.strip() if not key in ignore_attributes: principal_attributes[key] = value return principal_attributes def get_privs(self): """ Retrieve privileges of the current principal. Returns: a list of privileges Example: connection.get_privs() -> [ "GET", "ADD", "MODIFY", "DELETE" ] """ output = self.execute("get_privs") # one line of output is expected if len(output) > 1: raise KrbException("unexpected output of get_privs: " + output[1]) # detect error message if output[0].find("kadmin:") == 0: raise KrbException("get_privs returned error: " + output[0]) # parse output by removing the prefix and splitting it around spaces if output[0][:20] != "current privileges: ": raise KrbException("malformed get_privs output: " + output[0]) privs = output[0][20:].split(" ") return privs def add_principal(self, principal, password): """ Create a new principal. Parameters: principal - the name of the principal password - the principal's initial password Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame") """ # exec the add_principal command if password.find('"') == -1: self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n') # fools at MIT didn't bother implementing escaping, so passwords # that contain double quotes must be treated specially else: self.kadm_in.write('add_principal "' + principal + '"\n') self.kadm_in.write(password + "\n" + password + "\n") # send request and read response self.kadm_in.flush() output = self.read_result() # verify output created = False for line in output: # ignore NOTICE lines if line.find("NOTICE:") == 0: continue # ignore prompts elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0: continue # record whether success message was encountered elif line.find("Principal") == 0 and line.find("created.") != 0: created = True # error messages elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0: # principal exists if line.find("already exists") != -1: raise KrbException("principal already exists") # misc errors else: raise KrbException(line) # unknown output else: raise KrbException("unexpected add_principal output: " + line) # ensure success message was received if not created: raise KrbException("kadmin did not acknowledge principal creation") def delete_principal(self, principal): """ Delete a principal. Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA") """ # exec the delete_principal command and read response self.kadm_in.write('delete_principal -force "' + principal + '"\n') self.kadm_in.flush() output = self.read_result() # verify output deleted = False for line in output: # ignore reminder if line.find("Make sure that") == 0: continue # record whether success message was encountered elif line.find("Principal") == 0 and line.find("deleted.") != -1: deleted = True # error messages elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0: # principal exists if line.find("does not exist") != -1: raise KrbException("principal does not exist") # misc errors else: raise KrbException(line) # unknown output else: raise KrbException("unexpected delete_principal output: " + line) # ensure success message was received if not deleted: raise KrbException("did not receive principal deleted") def change_password(self, principal, password): """ Changes a principal's password. Example: connection.change_password("mspang@CSCLUB.UWATERLOO.CA", "opensesame") """ # exec the add_principal command if password.find('"') == -1: self.kadm_in.write('change_password -pw "' + password + '" "' + principal + '"\n') else: self.kadm_in.write('change_password "' + principal + '"\n') self.kadm_in.write(password + "\n" + password + "\n") # send request and read response self.kadm_in.flush() output = self.read_result() # verify output changed = False for line in output: # ignore NOTICE lines if line.find("NOTICE:") == 0: continue # ignore prompts elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0: continue # record whether success message was encountered elif line.find("Password") == 0 and line.find("changed.") != 0: changed = True # error messages elif line.find("change_password:") == 0 or line.find("kadmin:") == 0: raise KrbException(line) # unknown output else: raise KrbException("unexpected change_password output: " + line) # ensure success message was received if not changed: raise KrbException("kadmin did not acknowledge password change") ### Tests ### if __name__ == '__main__': from csc.common.test import * import random conffile = '/etc/csc/kerberos.cf' cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ]) principal = cfg['admin_principal'][1:-1] keytab = cfg['admin_keytab'][1:-1] realm = cfg['realm'][1:-1] # t=test p=principal e=expected tpname = 'testpirate' + '@' + realm tpw = str(random.randint(10**30, 10**31-1)) + 'YAR!' eprivs = ['GET', 'ADD', 'MODIFY', 'DELETE'] test(KrbConnection) connection = KrbConnection() success() test(connection.connect) connection.connect(principal, keytab) success() try: connection.delete_principal(tpname) except KrbException: pass test(connection.connected) assert_equal(True, connection.connected()) success() test(connection.add_principal) connection.add_principal(tpname, tpw) success() test(connection.list_principals) pals = connection.list_principals() assert_equal(True, tpname in pals) success() test(connection.get_privs) privs = connection.get_privs() assert_equal(eprivs, privs) success() test(connection.get_principal) princ = connection.get_principal(tpname) assert_equal(tpname, princ['Principal']) success() test(connection.delete_principal) connection.delete_principal(tpname) assert_equal(None, connection.get_principal(tpname)) success() test(connection.disconnect) connection.disconnect() assert_equal(False, connection.connected()) success()