Moved files into their new locations prior to commit of 0.2.
[public/pyceo-broken.git] / pylib / csc / backends / krb.py
1 # $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
2 """
3 Kerberos Backend Interface
4
5 This module is intended to be a thin wrapper around Kerberos operations.
6 Methods on the connection object correspond in a straightforward way to
7 calls to the Kerberos Master server.
8
9 A Kerberos principal is the second half of a CSC UNIX account. The principal
10 stores the user's password and and is used for all authentication on CSC
11 systems. Accounts that do not authenticate (e.g. club accounts) do not need
12 a Kerberos principal.
13
14 Unfortunately, there are no Python bindings to libkadm at this time. As a
15 temporary workaround, This module communicates with the kadmin CLI interface
16 via a pseudoterminal and pipe.
17 """
18 import os
19 import ipc
20
21
22 class KrbException(Exception):
23     """Exception class for all Kerberos-related errors."""
24     pass
25
26
27 class KrbConnection(object):
28     """
29     Connection to the Kerberos master server (kadmind). All Kerberos
30     principal updates are made via this class.
31
32     Exceptions: (all methods)
33         KrbException - on query/update failure
34
35     Example:
36         connection = KrbConnection()
37         connection.connect(...)
38
39         # make queries and updates, e.g.
40         connection.delete_principal("mspang")
41
42         connection.disconnect()
43     """
44
45     def __init__(self):
46         self.pid = None
47     
48
49     def connect(self, principal, keytab):
50         """
51         Establishes the connection to the Kerberos master server.
52
53         Parameters:
54             principal - the Kerberos princiapl to authenticate as
55             keytab    - keytab filename for authentication
56
57         Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab')
58         """
59
60         # check keytab
61         if not os.access(keytab, os.R_OK):
62             raise KrbException("cannot access Kerberos keytab: %s" % keytab)
63         
64         # command to run
65         kadmin = '/usr/sbin/kadmin'
66         kadmin_args = ['kadmin', '-p', principal, '-kt', keytab]
67         
68         # fork the kadmin command
69         self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args)
70         
71         # read welcome messages
72         welcome = self.read_result()
73         
74         # sanity checks on welcome messages
75         for line in welcome:
76             
77             # ignore auth message
78             if line.find("Authenticating") == 0:
79                 continue
80
81             # ignore log file message
82             elif line.find("kadmin.log") != -1:
83                 continue
84
85             # error message?
86             else:
87                 raise KrbException("unexpected kadmin output: " + welcome[0])
88     
89     
90     def disconnect(self):
91         """Close the connection to the master server."""
92         
93         if self.pid:
94             
95             # close the pipe connected to kadmin's standard input
96             self.kadm_in.close()
97             
98             # close the master pty connected to kadmin's stdout
99             try:
100                 self.kadm_out.close()
101             except OSError:
102                 pass
103
104             # wait for kadmin to terminate
105             os.waitpid(self.pid, 0)
106             self.pid = None
107
108
109     def connected(self):
110         """Determine whether the connection has been established."""
111
112         return self.pid != None
113
114
115
116     ### Helper Methods ###
117     
118     def read_result(self):
119         """
120         Helper function to read output of kadmin until it
121         prompts for input.
122
123         Returns: a list of lines returned by kadmin
124         """
125
126         # list of lines output by kadmin
127         result = []
128
129         # the kadmin prompt that signals the end output
130         # note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
131         prompt = "kadmin:"
132
133         # timeout variables. the timeout will start at timeout and
134         # increase up to max_timeout when read() returns nothing (i.e., times out)
135         timeout = 0.01
136         timeout_increment = 0.10
137         timeout_maximum = 1.00
138         
139         # input loop: read from kadmin until the kadmin prompt
140         buffer = ''
141         while True:
142             
143             # attempt to read any available data
144             data = self.kadm_out.read(block=False, timeout=timeout)
145             buffer += data
146
147             # nothing was read
148             if data == '':
149                 
150                 # so wait longer for data next time
151                 if timeout < timeout_maximum:
152                     timeout += timeout_increment
153                     continue
154
155                 # give up after too much waiting
156                 else:
157
158                     # check kadmin status
159                     status = os.waitpid(self.pid, os.WNOHANG)
160                     if status[0] == 0:
161
162                         # kadmin still alive
163                         raise KrbException("timeout while reading response from kadmin")
164
165                     else:
166
167                         # kadmin died!
168                         raise KrbException("kadmin died while reading response")
169
170             # break into lines and save all but the final
171             # line (which is incomplete) into result
172             lines = buffer.split("\n")
173             buffer = lines[-1]
174             lines = lines[:-1]
175             for line in lines:
176                 line = line.strip()
177                 result.append(line)
178            
179             # if the incomplete lines in the buffer is the kadmin prompt,
180             # then the result is complete and may be returned
181             if buffer.strip() == prompt:
182                 break
183
184         return result
185     
186     
187     def execute(self, command):
188         """
189         Helper function to execute a kadmin command.
190
191         Parameters:
192             command - the command to execute
193         
194         Returns: a list of lines output by the command
195         """
196         
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)
202         
203         # send the command to kadmin
204         self.kadm_in.write(command + "\n")
205         self.kadm_in.flush()
206         
207         # read the command output and return it
208         result = self.read_result()
209         return result
210     
211
212     
213     ### Commands ###
214     
215     def list_principals(self):
216         """
217         Retrieve a list of Kerberos principals.
218
219         Returns: a list of principals
220
221         Example: connection.list_principals() -> [
222                      "ceo/admin@CSCLUB.UWATERLOO.CA",
223                      "sysadmin/admin@CSCLUB.UWATERLOO.CA",
224                      "mspang@CSCLUB.UWATERLOO.CA",
225                  ]
226         
227         """
228         
229         principals = self.execute("list_principals")
230
231         # assuming that there at least some host principals
232         if len(principals) < 1:
233             raise KrbException("no kerberos principals")
234
235         # detect error message
236         if principals[0].find("kadmin:") == 0:
237             raise KrbException("list_principals returned error: " + principals[0])
238
239         # verify principals are well-formed
240         for principal in principals:
241             if principal.find("@") == -1:
242                 raise KrbException('malformed pricipal: "' + principal + '"')
243
244         return principals
245     
246     
247     def get_principal(self, principal):
248         """
249         Retrieve principal details.
250
251         Returns: a dictionary of principal attributes
252
253         Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> {
254                      "Principal": "ceo/admin@CSCLUB.UWATERLOO.CA",
255                      "Policy": "[none]",
256                      ...
257                  }
258         """
259         
260         output = self.execute('get_principal "' + principal + '"')
261         
262         # detect error message
263         if output[0].find("kadmin:") == 0:
264             raise KrbException("get_principal returned error: " + output[0])
265
266         # detect more errors
267         if output[0].find("get_principal: ") == 0:
268             
269             message = output[0][15:]
270             
271             # principal does not exist => None
272             if message.find("Principal does not exist") == 0:
273                 return None
274
275         # dictionary to store attributes
276         principal_attributes = {}
277
278         # attributes that will not be returned
279         ignore_attributes = ['Key']
280
281         # split output into a dictionary of attributes
282         for line in output:
283             key, value = line.split(":", 1)
284             value = value.strip()
285             if not key in ignore_attributes:
286                 principal_attributes[key] = value
287                 
288         return principal_attributes
289     
290     
291     def get_privs(self):
292         """
293         Retrieve privileges of the current principal.
294         
295         Returns: a list of privileges
296
297         Example: connection.get_privs() ->
298                      [ "GET", "ADD", "MODIFY", "DELETE" ]
299         """
300         
301         output = self.execute("get_privs")
302
303         # one line of output is expected
304         if len(output) > 1:
305             raise KrbException("unexpected output of get_privs: " + output[1])
306
307         # detect error message
308         if output[0].find("kadmin:") == 0:
309             raise KrbException("get_privs returned error: " + output[0])
310
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(" ")
315
316         return privs
317     
318     
319     def add_principal(self, principal, password):
320         """
321         Create a new principal.
322
323         Parameters:
324             principal - the name of the principal
325             password  - the principal's initial password
326         
327         Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
328         """
329
330         # exec the add_principal command
331         if password.find('"') == -1:
332             self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n')
333             
334         # fools at MIT didn't bother implementing escaping, so passwords
335         # that contain double quotes must be treated specially
336         else:
337             self.kadm_in.write('add_principal "' + principal + '"\n')
338             self.kadm_in.write(password + "\n" + password + "\n")
339
340         # send request and read response
341         self.kadm_in.flush()
342         output = self.read_result()
343
344         # verify output
345         created = False
346         for line in output:
347
348             # ignore NOTICE lines
349             if line.find("NOTICE:") == 0:
350                 continue
351
352             # ignore prompts
353             elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
354                 continue
355
356             # record whether success message was encountered
357             elif line.find("Principal") == 0 and line.find("created.") != 0:
358                 created = True
359
360             # error messages
361             elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0:
362                 
363                 # principal exists
364                 if line.find("already exists") != -1:
365                     raise KrbException("principal already exists")
366
367                 # misc errors
368                 else:
369                     raise KrbException(line)
370
371             # unknown output
372             else:
373                 raise KrbException("unexpected add_principal output: " + line)
374            
375         # ensure success message was received
376         if not created:
377             raise KrbException("did not receive principal created in response")
378     
379     
380     def delete_principal(self, principal):
381         """
382         Delete a principal.
383
384         Parameters:
385             principal - the principal name
386
387         Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
388         """
389         
390         # exec the delete_principal command and read response
391         self.kadm_in.write('delete_principal -force "' + principal + '"\n')
392         self.kadm_in.flush()
393         output = self.read_result()
394
395         # verify output
396         deleted = False
397         for line in output:
398
399             # ignore reminder
400             if line.find("Make sure that") == 0:
401                 continue
402
403             # record whether success message was encountered
404             elif line.find("Principal") == 0 and line.find("deleted.") != -1:
405                 deleted = True
406
407             # error messages
408             elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0:
409                 
410                 # principal exists
411                 if line.find("does not exist") != -1:
412                     raise KrbException("principal does not exist")
413
414                 # misc errors
415                 else:
416                     raise KrbException(line)
417
418             # unknown output
419             else:
420                 raise KrbException("unexpected delete_principal output: " + line)
421            
422         # ensure success message was received
423         if not deleted:
424             raise KrbException("did not receive principal deleted")
425         
426
427
428 ### Tests ###
429
430 if __name__ == '__main__':
431     PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
432     KEYTAB = 'ceo.keytab'
433     
434     connection = KrbConnection()
435     print "running disconnect()"
436     connection.disconnect()
437     print "running connect('%s', '%s')" % (PRINCIPAL, KEYTAB)
438     connection.connect(PRINCIPAL, KEYTAB)
439     print "running list_principals()", "->", "[" + ", ".join(map(repr,connection.list_principals()[0:3])) + " ...]"
440     print "running get_privs()", "->", str(connection.get_privs())
441     print "running add_principal('testtest', 'BLAH')"
442     connection.add_principal("testtest", "FJDSLDLFKJSF")
443     print "running get_principal('testtest')", "->", '(' + connection.get_principal("testtest")['Principal'] + ')'
444     print "running delete_principal('testtest')"
445     connection.delete_principal("testtest")
446     print "running disconnect()"
447     connection.disconnect()
448