449 lines
14 KiB
Python
449 lines
14 KiB
Python
# $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
|
|
"""
|
|
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 pseudoterminal and 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 != 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 = []
|
|
|
|
# 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
|
|
buffer = ''
|
|
while True:
|
|
|
|
# attempt to read any available data
|
|
data = self.kadm_out.read(block=False, timeout=timeout)
|
|
buffer += 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")
|
|
|
|
# break into lines and save all but the final
|
|
# line (which is incomplete) into result
|
|
lines = buffer.split("\n")
|
|
buffer = lines[-1]
|
|
lines = lines[:-1]
|
|
for line in lines:
|
|
line = line.strip()
|
|
result.append(line)
|
|
|
|
# if the incomplete lines in the buffer is the kadmin prompt,
|
|
# then the result is complete and may be returned
|
|
if buffer.strip() == prompt:
|
|
break
|
|
|
|
return result
|
|
|
|
|
|
def execute(self, command):
|
|
"""
|
|
Helper function to execute a kadmin command.
|
|
|
|
Parameters:
|
|
command - the command to execute
|
|
|
|
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("did not receive principal created in response")
|
|
|
|
|
|
def delete_principal(self, principal):
|
|
"""
|
|
Delete a principal.
|
|
|
|
Parameters:
|
|
principal - the principal name
|
|
|
|
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")
|
|
|
|
|
|
|
|
### Tests ###
|
|
|
|
if __name__ == '__main__':
|
|
PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
|
|
KEYTAB = 'ceo.keytab'
|
|
|
|
connection = KrbConnection()
|
|
print "running disconnect()"
|
|
connection.disconnect()
|
|
print "running connect('%s', '%s')" % (PRINCIPAL, KEYTAB)
|
|
connection.connect(PRINCIPAL, KEYTAB)
|
|
print "running list_principals()", "->", "[" + ", ".join(map(repr,connection.list_principals()[0:3])) + " ...]"
|
|
print "running get_privs()", "->", str(connection.get_privs())
|
|
print "running add_principal('testtest', 'BLAH')"
|
|
connection.add_principal("testtest", "FJDSLDLFKJSF")
|
|
print "running get_principal('testtest')", "->", '(' + connection.get_principal("testtest")['Principal'] + ')'
|
|
print "running delete_principal('testtest')"
|
|
connection.delete_principal("testtest")
|
|
print "running disconnect()"
|
|
connection.disconnect()
|
|
|