pyceo/pylib/csc/backends/krb.py

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()