d0b2c878996b185b2dcedf0df17afbb67130d199
[public/pyceo-broken.git] / pylib / csc / adm / accounts.py
1 """
2 UNIX Accounts Administration
3
4 This module contains functions for creating, deleting, and manipulating
5 UNIX user accounts and account groups in the CSC LDAP directory.
6 """
7 import re, pwd, grp, os
8 from csc.common import conf
9 from csc.common.excep import InvalidArgument
10 from csc.backends import ldapi, krb
11
12
13 ### Configuration ###
14
15 CONFIG_FILE = '/etc/csc/accounts.cf'
16
17 cfg = {}
18
19 def configure():
20     """Helper to load the accounts configuration. You need not call this."""
21     
22     string_fields = [ 'member_shell', 'member_home', 'member_desc',
23             'member_group', 'club_shell', 'club_home', 'club_desc',
24             'club_group', 'admin_shell', 'admin_home', 'admin_desc',
25             'admin_group', 'group_desc', 'username_regex', 'groupname_regex',
26             'shells_file', 'server_url', 'users_base', 'groups_base',
27             'admin_bind_dn', 'admin_bind_pw', 'realm', 'admin_principal',
28             'admin_keytab' ]
29     numeric_fields = [ 'member_min_id', 'member_max_id', 'club_min_id',
30             'club_max_id', 'admin_min_id', 'admin_max_id', 'group_min_id',
31             'group_max_id', 'min_password_length' ]
32
33     # read configuration file
34     cfg_tmp = conf.read(CONFIG_FILE)
35
36     # verify configuration (not necessary, but prints a useful error)
37     conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
38     conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
39
40     # update the current configuration with the loaded values
41     cfg.update(cfg_tmp)
42
43
44
45 ### Exceptions  ###
46
47 KrbException = krb.KrbException
48 LDAPException = ldapi.LDAPException
49 ConfigurationException = conf.ConfigurationException
50
51 class AccountException(Exception):
52     """Base exception class for account-related errors."""
53
54 class NoAvailableIDs(AccountException):
55     """Exception class for exhausted userid ranges."""
56     def __init__(self, minid, maxid):
57         self.minid, self.maxid = minid, maxid
58     def __str__(self):
59         return "No free ID pairs found in range [%d, %d]" % (self.minid, self.maxid)
60
61 class NameConflict(AccountException):
62     """Exception class for name conflicts with existing accounts/groups."""
63     def __init__(self, name, nametype, source):
64         self.name, self.nametype, self.source = name, nametype, source
65     def __str__(self):
66         return 'Name Conflict: %s "%s" already exists in %s' % (self.nametype, self.name, self.source)
67
68 class NoSuchAccount(AccountException):
69     """Exception class for missing LDAP entries for accounts."""
70     def __init__(self, account, source):
71         self.account, self.source = account, source
72     def __str__(self):
73         return 'Account "%s" not found in %s' % (self.account, self.source)
74
75 class NoSuchGroup(AccountException):
76     """Exception class for missing LDAP entries for groups."""
77     def __init__(self, account, source):
78         self.account, self.source = account, source
79     def __str__(self):
80         return 'Account "%s" not found in %s' % (self.account, self.source)
81     
82
83
84 ### Connection Management ###
85
86 ldap_connection = ldapi.LDAPConnection()
87 krb_connection = krb.KrbConnection()
88
89 def connect():
90     """Connect to LDAP and Kerberos and load configuration. You must call before anything else."""
91
92     configure()
93
94     # connect to the LDAP server
95     ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
96
97     # connect to the Kerberos master server
98     krb_connection.connect(cfg['admin_principal'], cfg['admin_keytab'])
99
100
101 def disconnect():
102     """Disconnect from LDAP and Kerberos. Call this before quitting."""
103
104     ldap_connection.disconnect()
105     krb_connection.disconnect()
106
107
108 def connected():
109     """Determine whether a connection has been established."""
110
111     return ldap_connection.connected() and krb_connection.connected()
112
113
114
115 ### General Account Management ###
116
117 def create(username, name, minimum_id, maximum_id, home, password=None, description='', gecos='', shell=None, group=None):
118     """
119     Creates a UNIX user account. This involves first creating an LDAP
120     directory entry, then creating a Kerberos principal.
121
122     The UID/GID namespace may be divided into ranges according to account type
123     or purpose. This function requires such a range to allocate ids from.
124
125     If no password is specified or password is None, no Kerberos principal
126     will be created and the account will not be capable of direct login.
127     This is desirable for administrative and club accounts.
128
129     If no group is specified, a new group will be created with the same name
130     as the user. The uid of the created user and gid of the created group
131     will be numerically equal. There is generally no reason to specify a
132     group. Furthermore, only groups present in the directory are allowed.
133
134     If an account is relevant to only one system and will not own files on
135     NFS, please use adduser(8) on the relevant system instead.
136
137     Generally do not directly use this function. The create_member(),
138     create_club(), and create_adm() functions will fill in most of
139     the details for you and may do additional checks.
140
141     Parameters:
142         username    - UNIX username for the account
143         name        - common name LDAP attribute
144         minimum_id  - the smallest UID/GID to assign
145         maximum_id  - the largest UID/GID to assign
146         home        - home directory LDAP attribute
147         password    - password for the account
148         description - description LDAP attribute
149         gecos       - gecos LDAP attribute
150         shell       - user shell LDAP attribute
151         group       - primary group for account
152
153     Exceptions:
154         NameConflict     - when the name conflicts with an existing account
155         NoSuchGroup      - when the group parameter corresponds to no group
156         NoAvailableIDs   - when the ID range is exhausted
157         AccountException - when not connected
158
159     Returns: the uid number of the new account
160
161     Example: create('mspang', 'Michael Spang', 20000, 39999,
162                  '/users/mspang', 'secret', 'CSC Member Account',
163                  build_gecos('Michael Spang', other='3349'),
164                  '/bin/bash', 'users')
165     """
166  
167     # check connection
168     if not connected():
169         raise AccountException("Not connected to LDAP and Kerberos")
170
171     # check for path characters in username (. and /)
172     if re.search('[\\./]', username):
173         raise InvalidArgument("username", username, "invalid characters")
174
175     check_name_usage(username)
176
177     # determine the first available userid
178     userid = first_available_id(minimum_id, maximum_id)
179     if not userid:
180         raise NoAvailableIDs(minimum_id, maximum_id)
181
182     # determine the account's default group
183     if group: 
184         group_data = ldap_connection.group_lookup(group)
185         if not group_data:
186             raise NoSuchGroup(group, "LDAP")
187         gid = int(group_data['gidNumber'][0])
188     else:
189         gid = userid
190
191     ### User creation ###
192
193     # create the LDAP entry
194     ldap_connection.user_add(username, name, userid, gid, home, shell, gecos, description)
195
196     # create a user group if no other group was specified
197     if not group:
198         ldap_connection.group_add(username, gid)
199
200     # create the Kerberos principal
201     if password:    
202         principal = username + '@' + cfg['realm']
203         krb_connection.add_principal(principal, password)
204
205     return userid
206
207
208 def delete(username):
209     """
210     Deletes a UNIX account. Both LDAP entries and Kerberos principals that
211     match username are deleted. A group with the same name is deleted too,
212     if it exists and has the same id as the account.
213
214     Returns: tuple with deleted LDAP and Kerberos information
215              note: the Kerberos keys are not recoverable 
216     """
217
218     # check connection
219     if not connected():
220         raise AccountException("Not connected to LDAP and Kerberos")
221
222     # build principal name from username
223     principal = username + '@' + cfg['realm']
224
225     # get account state 
226     ldap_state = ldap_connection.user_lookup(username)
227     krb_state = krb_connection.get_principal(principal)
228     group_state = ldap_connection.group_lookup(username)
229
230     # don't delete group unless the gid matches the account's uid
231     if not ldap_state or group_state and ldap_state['uidNumber'][0] != group_state['gidNumber'][0]:
232         group_state = None
233
234     # fail if no data is found in either LDAP or Kerberos
235     if not ldap_state and not krb_state:
236         raise NoSuchAccount(username, "LDAP/Kerberos")
237
238     ### User deletion ###
239
240     # delete the LDAP entries
241     if ldap_state:
242         ldap_connection.user_delete(username)
243     if group_state:
244         ldap_connection.group_delete(username)
245
246     # delete the Kerberos principal
247     if krb_state:
248         krb_connection.delete_principal(principal)
249
250     return ldap_state, group_state, krb_state
251
252
253 def status(username):
254     """
255     Checks if an account exists.
256
257     Returns: a boolean 2-tuple (exists, has_password)
258     """
259
260     ldap_state = ldap_connection.user_lookup(username)
261     krb_state = krb_connection.get_principal(username)
262     return (ldap_state is not None, krb_state is not None)
263
264
265 def add_password(username, password):
266     """
267     Creates a principal for an existing, passwordless account.
268
269     Parameters:
270         username - a UNIX account username
271         password - a password for the acccount
272     """
273     check_account_status(username)
274     ldap_state = ldap_connection.user_lookup(username)
275     if int(ldap_state['uidNumber'][0]) < 1000:
276         raise AccountException("Attempted to add password to a system account")
277     krb_connection.add_principal(username, password)
278
279
280 def reset_password(username, newpassword):
281     """
282     Changes a user's password.
283
284     Parameters:
285         username    - a UNIX account username
286         newpassword - a new password for the account
287     """
288     check_account_status(username, require_krb=True)
289     krb_connection.change_password(username, newpassword)
290
291
292 def get_uid(username):
293     """
294     Determine the numeric uid of an account.
295
296     Returns: a uid as an int
297     """
298     check_account_status(username)
299     account_data = ldap_connection.user_lookup(username)
300     return int(account_data['uidNumber'][0])
301
302
303 def get_gid(username):
304     """
305     Determine the numeric gid of an account (default group).
306
307     Returns: a gid as an int
308     """
309     check_account_status(username)
310     account_data = ldap_connection.user_lookup(username)
311     return int(account_data['gidNumber'][0])
312
313
314 def get_gecos(username, account_data=None):
315     """
316     Retrieve GECOS information of a user.
317
318     Returns: raw gecos data as a string, or None
319     """
320     check_account_status(username)
321     if not account_data:
322         account_data = ldap_connection.user_lookup(username)
323     if 'gecos' in account_data:
324         return account_data['gecos'][0]
325     else:
326         return None
327     
328
329 def update_gecos(username, gecos_data):
330     """
331     Set GECOS information for a user. The LDAP 'cn' attribute
332     is also updated with the user's full name.
333
334     See build_gecos() and parse_gecos() for help dealing with
335     the chfn(1) GEOCS format.
336
337     Use update_name() to update the name porition, as it will update
338     the LDAP 'cn' atribute as well.
339
340     Parameters:
341         username   - a UNIX account username
342         gecos_data - a raw gecos string
343
344     Example: update_gecos('mspang', build_gecos('Mike Spang'))
345     """
346     check_account_status(username)
347     entry = ldap_connection.user_lookup(username)
348     entry['gecos'] = [ gecos_data ]
349     ldap_connection.user_modify(username, entry)
350
351
352 def get_name(username):
353     """
354     Get the real name of a user. Note that this name is usually stored
355     in both the 'cn' attribute and the 'gecos' attribute, and they
356     may differ. This function will always return the first in the'cn'
357     version. If there are multiple, the first in the list is returned.
358
359     Returns: the common name associated with the account
360     """
361     check_account_status(username)
362     account_data = ldap_connection.user_lookup(username)
363     return account_data['cn'][0]
364
365
366 def update_name(username, name, update_gecos=True):
367     """
368     Set the real name of a user. This name will be updated in both
369     the GECOS field and the common name field. If there are multiple
370     common names, they will *all* be overwritten with the provided name.
371
372     Parameters:
373         username     - the UNIX account usernmae
374         nane         - new real name for the account
375         update_gecos - whether to update gecos field
376     """
377     check_account_status(username)
378     account_data = ldap_connection.user_lookup(username)
379     account_data['cn'] = [ name ]
380     if update_gecos:
381         gecos_dict = parse_gecos(get_gecos(username, account_data))
382         gecos_dict['fullname'] = name
383         account_data['gecos'] = [ build_gecos(**gecos_dict) ]
384     ldap_connection.user_modify(username, account_data)
385
386
387 def get_shell(username):
388     """
389     Retrieve a user's shell.
390
391     Returns: the path to the shell, or None
392     """
393     check_account_status(username)
394     account_data = ldap_connection.user_lookup(username)
395     if 'loginShell' not in account_data or len(account_data['loginShell']) < 1:
396         return None
397     return account_data['loginShell'][0]
398
399
400 def update_shell(username, shell, check=True):
401     """
402     Set a user's shell.
403
404     Parameters:
405         username - the UNIX account username
406         shell    - the new shell for the user
407         check    - whether to check if the shell is in the shells file
408
409     Exceptions:
410         InvalidArgument - on nonexistent shell
411     """
412
413     # reject nonexistent or nonexecutable shells
414     if not os.access(shell, os.X_OK) or not os.path.isfile(shell):
415         raise InvalidArgument("shell", shell, "is not a regular executable file")
416
417     if check:
418         
419         # load shells file
420         shells = open(cfg['shells_file']).read().split("\n")
421         shells = [ x for x in shells if x and x[0] == '/' and '#' not in x ]
422
423         # reject shells that aren't in the shells file (usually /etc/shells)
424         if check and shell not in shells:
425             raise InvalidArgument("shell", shell, "is not in %s" % cfg['shells_file'])
426     
427     check_account_status(username)
428     account_data = ldap_connection.user_lookup(username)
429     account_data['loginShell'] = [ shell ]
430     ldap_connection.user_modify(username, account_data)
431     
432
433 def get_home(username):
434     """
435     Get the home directory of a user.
436
437     Returns: path to the user's home directory
438     """
439     check_account_status(username)
440     account_data = ldap_connection.user_lookup(username)
441     return account_data['homeDirectory'][0]
442
443
444 def update_home(username, home):
445     """
446     Set the home directory of a user.
447
448     Parameters:
449         username - the UNIX account username
450         home     - new home directory for the user
451     """
452     check_account_status(username)
453     if not home[0] == '/':
454         raise InvalidArgument('home', home, 'relative path')
455     account_data = ldap_connection.user_lookup(username)
456     account_data['homeDirectory'] = [ home ]
457     ldap_connection.user_modify(username, account_data)
458
459
460
461 ### General Group Management ###
462
463 def create_group(groupname, minimum_id=None, maximum_id=None, description=''):
464     """
465     Creates a UNIX group. This involves adding an entry to LDAP.
466
467     The UID/GID namespace may be divided into ranges according to group
468     type or purpose. This function accept such a range to allocate ids from.
469     If none is specified, it will use the default from the configuration file.
470
471     If a group needs directory accounts as members, or if the group will
472     own files on NFS, you must add it to the directory with this function.
473
474     If a group is relevant to only a single system and does not need any
475     directory accounts as members, create it with the addgroup(8) utility
476     for just that system instead.
477
478     If you do not specify description, the default will be used. If no
479     description at all is wanted, set description to None.
480
481     Parameters:
482         groupname   - UNIX group name
483         minimum_id  - the smallest GID to assign
484         maximum_id  - the largest GID to assign
485         description - description LDAP attribute
486
487     Exceptions:
488         GroupExists    - when the group name conflicts with an existing group
489         NoAvailableIDs - when the ID range is exhausted
490         GroupException - when not connected
491         LDAPException  - on LDAP failure
492
493     Returns: the gid number of the new group
494
495     Example: create_group('ninjas', 10000, 14999)
496     """
497
498     # check connection
499     if not connected():
500         raise AccountException("Not connected to LDAP and Kerberos")
501
502     # check groupname format
503     if not groupname or not re.match(cfg['groupname_regex'], groupname):
504         raise InvalidArgument("groupname", groupname, "expected format %s" % repr(cfg['groupname_regex']))
505
506     # load defaults for unspecified parameters
507     if not minimum_id and maximum_id:
508         minimum_id = cfg['group_min_id']
509         maximum_id = cfg['group_max_id']
510     if description == '':
511         description = cfg['group_desc']
512
513     check_name_usage(groupname)
514
515     # determine the first available groupid
516     groupid = first_available_id(cfg['group_min_id'], cfg['group_max_id'])
517     if not groupid:
518         raise NoAvailableIDs(minimum_id, maximum_id)
519
520     ### Group creation ###
521
522     # create the LDAP entry
523     ldap_connection.group_add(groupname, groupid, description)
524
525     return groupid
526
527
528 def delete_group(groupname):
529     """
530     Deletes a group.     
531
532     Returns: the deleted LDAP information
533     """
534
535     # check connection
536     if not connected():
537         raise AccountException("Not connected to LDAP")
538
539     # get account state 
540     ldap_state = ldap_connection.group_lookup(groupname)
541
542     # fail if no data is found in either LDAP or Kerberos
543     if not ldap_state:
544         raise NoSuchGroup(groupname, "LDAP")
545
546     ### Group deletion ###
547
548     # delete the LDAP entry
549     if ldap_state:
550         ldap_connection.group_delete(groupname)
551
552     return ldap_state
553
554
555 def check_membership(username, groupname):
556     """
557     Determines whether an account is a member of a group
558     by checking the group's member list and the user's
559     default group.
560
561     Returns: True if username is a member of groupname
562     """
563
564     check_account_status(username)
565     check_group_status(groupname)
566
567     group_data = ldap_connection.group_lookup(groupname)
568     user_data = ldap_connection.user_lookup(username)
569
570     group_members = get_members(groupname, group_data)
571     group_id = int(group_data['gidNumber'][0])
572     user_group = int(user_data['gidNumber'][0])
573
574     return username in group_members or group_id == user_group
575     
576
577 def get_members(groupname, group_data=None):
578     """
579     Retrieve a list of members of a group. This list
580     will not include accounts that are members because
581     their gidNumber attribute matches the group's.
582
583     Parameters:
584         group_data - result of a previous LDAP lookup on groupname (internal)
585
586     Returns: a list of usernames
587     """
588
589     check_group_status(groupname)
590
591     if not group_data:
592         group_data = ldap_connection.group_lookup(groupname)
593
594     if 'memberUid' in group_data:
595         group_members = group_data['memberUid']
596     else:
597         group_members = []
598
599     return group_members
600
601     
602 def add_member(username, groupname):
603     """
604     Add an account to the list of group members.
605
606     Returns: False if the user was already a member, else True
607     """
608
609     check_account_status(username)
610     check_group_status(groupname)
611
612     group_data = ldap_connection.group_lookup(groupname)
613     group_members = get_members(groupname, group_data)
614
615     if groupname in group_members:
616         return False
617     
618     group_members.append(username)
619     group_data['memberUid'] = group_members
620     ldap_connection.group_modify(groupname, group_data)
621
622     return True
623
624
625 def remove_member(username, groupname):
626     """
627     Removes an account from the list of group members.
628
629     Returns: True if the user was a member, else False
630     """
631
632     check_account_status(username)
633     check_group_status(groupname)
634
635     group_data = ldap_connection.group_lookup(groupname)
636     group_members = get_members(groupname, group_data)
637
638     if username not in group_members:
639         return False
640
641     while username in group_members:
642         group_members.remove(username)
643
644     group_data['memberUid'] = group_members
645     ldap_connection.group_modify(groupname, group_data)
646
647     return True
648
649
650 ### Account Types ###
651
652 def create_member(username, password, name, memberid):
653     """
654     Creates a UNIX user account with options tailored to CSC members.
655
656     Note: The 'other' section of the GECOS field is filled with the CSC
657           memberid. This section cannot be changed by the user via chfn(1).
658
659     Parameters:
660         username - the desired UNIX username
661         password - the desired UNIX password
662         name     - the member's real name
663         memberid - the CSC member id number
664
665     Exceptions:
666         InvalidArgument - on bad account attributes provided
667
668     Returns: the uid number of the new account
669
670     See: create()
671     """
672
673     # check connection
674     if not connected():
675         raise AccountException("not connected to LDAP and Kerberos")
676
677     # check username format
678     if not username or not re.match(cfg['username_regex'], username):
679         raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
680
681     # check password length
682     if not password or len(password) < cfg['min_password_length']:
683         raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
684
685     minimum_id = cfg['member_min_id']
686     maximum_id = cfg['member_max_id']
687     home = cfg['member_home'] + '/' + username
688     description = cfg['member_desc']
689     gecos_field = build_gecos(name, other=memberid)
690     shell = cfg['member_shell']
691     group = cfg['member_group']
692
693     return create(username, name, minimum_id, maximum_id, home, password, description, gecos_field, shell, group)
694
695
696 def create_club(username, name, memberid):
697     """
698     Creates a UNIX user account with options tailored to CSC-hosted clubs.
699     
700     Note: The 'other' section of the GECOS field is filled with the CSC
701           memberid. This section cannot be changed by the user via chfn(1).
702
703     Parameters:
704         username - the desired UNIX username
705         name     - the club name
706         memberid - the CSC member id number
707
708     Exceptions:
709         InvalidArgument - on bad account attributes provided
710
711     Returns: the uid number of the new account
712
713     See: create()
714     """
715
716     # check connection
717     if not connected():
718         raise AccountException("not connected to LDAP and Kerberos")
719
720     # check username format
721     if not username or not re.match(cfg['username_regex'], username):
722         raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
723     
724     password = None
725     minimum_id = cfg['club_min_id']
726     maximum_id = cfg['club_max_id']
727     home = cfg['club_home'] + '/' + username
728     description = cfg['club_desc']
729     gecos_field = build_gecos(name, other=memberid)
730     shell = cfg['club_shell']
731     group = cfg['club_group']
732
733     return create(username, name, minimum_id, maximum_id, home, password, description, gecos_field, shell, group)
734
735
736 def create_adm(username, name):
737     """
738     Creates a UNIX user account with options tailored to long-lived
739     administrative accounts (e.g. vp, www, sysadmin, etc). 
740
741     Parameters:
742         username - the desired UNIX username
743         name     - a descriptive name or purpose
744
745     Exceptions:
746         InvalidArgument - on bad account attributes provided
747
748     Returns: the uid number of the new account
749
750     See: create()
751     """
752
753     # check connection
754     if not connected():
755         raise AccountException("not connected to LDAP and Kerberos")
756
757     # check username format
758     if not username or not re.match(cfg['username_regex'], username):
759         raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
760
761     password = None
762     minimum_id = cfg['admin_min_id']
763     maximum_id = cfg['admin_max_id']
764     home = cfg['admin_home'] + '/' + username
765     description = cfg['admin_desc']
766     gecos_field = build_gecos(name)
767     shell = cfg['admin_shell']
768     group = cfg['admin_group']
769
770     return create(username, name, minimum_id, maximum_id, home, password, description, gecos_field, shell, group)
771
772
773
774 ### Miscellaneous Helpers ###
775
776 def check_name_usage(name):
777     """
778     Helper function: Ensures a user or group name does not exist in either
779     Kerberos, LDAP, or through calls to libc and NSS. This is used prior to
780     creating an accout or group to determine if the name is free.
781
782     Parameters:
783         name - the user or group name to check for
784
785     Exceptions:
786         NameConflict - if the name was found anywhere
787     """
788
789     # see if user exists in LDAP
790     if ldap_connection.user_lookup(name):
791         raise NameConflict(name, "account", "LDAP")
792     
793     # see if group exists in LDAP
794     if ldap_connection.group_lookup(name):
795         raise NameConflict(name, "group", "LDAP")
796
797     # see if user exists in Kerberos
798     principal = name + '@' + cfg['realm']
799     if krb_connection.get_principal(principal):
800         raise NameConflict(name, "account", "KRB")
801
802     # see if user exists by getpwnam(3)
803     try:
804         pwd.getpwnam(name)
805         raise NameConflict(name, "account", "NSS")
806     except KeyError:
807         pass
808
809     # see if group exists by getgrnam(3)
810     try:
811         grp.getgrnam(name)
812         raise NameConflict(name, "group", "NSS")
813     except KeyError:
814         pass
815
816
817 def check_account_status(username, require_ldap=True, require_krb=False):
818     """Helper function to verify that an account exists."""
819     
820     if not connected(): 
821         raise AccountException("Not connected to LDAP and Kerberos")
822     if require_ldap and not ldap_connection.user_lookup(username):
823         raise NoSuchAccount(username, "LDAP")
824     if require_krb and not krb_connection.get_principal(username):
825         raise NoSuchAccount(username, "KRB")
826
827
828 def check_group_status(groupname):
829     """Helper function to verify that a group exists."""
830     
831     if not connected(): 
832         raise AccountException("Not connected to LDAP and Kerberos")
833     if not ldap_connection.group_lookup(groupname):
834         raise NoSuchGroup(groupname, "LDAP")
835
836
837 def parse_gecos(gecos_data):
838     """
839     Build a dictionary out of a chfn(1) style GECOS string.
840
841     Parameters:
842         gecos_data - a gecos string formatted by chfn(1)
843
844     Returns: a dictinoary of components
845     
846     Example: parse_gecos('Michael Spang,,,') -> {
847                  'fullname': 'Michael Spang',
848                  'roomnumber': '',
849                  'workphone': '',
850                  'homephone': '',
851                  'other': None
852              }
853     """
854     
855     # silently remove erroneous colons
856     while ':' in gecos_data:
857         index = gecos_data.find(':')
858         gecos_data = gecos_data[:index] + gecos_data[index+1:]
859
860     gecos_vals = gecos_data.split(',', 4)
861     gecos_vals.extend([ None ] * (5-len(gecos_vals)))
862     gecos_keys = ['fullname', 'roomnumber', 'workphone',
863                   'homephone', 'other' ]
864     return dict((gecos_keys[i], gecos_vals[i]) for i in xrange(5))
865
866
867 def build_gecos(fullname=None, roomnumber=None, workphone=None, homephone=None, other=None):
868     """
869     Build a chfn(1)-style GECOS field from its components.
870
871     See: chfn(1)
872     
873     Parameters:
874         fullname   - GECOS full name
875         roomnumber - GECOS room number
876         workphone  - GECOS work phone
877         homephone  - GECOS home phone
878         other      - GECOS other
879
880     Returns: string appropriate for a GECOS field value
881     """
882
883     # check first four params for illegal chars
884     args = (fullname, roomnumber, workphone, homephone)
885     names = ('fullname', 'roomnumber', 'workphone', 'homephone')
886     for index in xrange(4):
887         for badchar in (',', ':', '='):
888             if args[index] and badchar in str(args[index]):
889                 raise InvalidArgument(names[index], args[index], "invalid characters")
890
891     # check other for illegal chars
892     if other and ':' in str(other):
893         raise InvalidArgument('other', other, "invalid characters")
894     
895     # append each field
896     if fullname is not None:
897         gecos_data = str(fullname)
898     for field in (roomnumber, workphone, homephone, other):
899         if field is not None:
900             gecos_data += ',' + str(field)
901
902     return gecos_data
903
904
905 def check_id_nss(ugid):
906     """Helper to ensure there is no account or group with an ID."""
907
908     try:
909         pwd.getpwuid(ugid)
910         return False
911     except KeyError:
912         pass
913
914     try:
915         grp.getgrgid(ugid)
916         return False
917     except KeyError:
918         pass
919
920     return True
921
922
923 def first_available_id(minimum, maximum):
924     """
925     Determines the first available id within a range.
926
927     To be "available", there must be neither a user
928     with the id nor a group with the id.
929
930     Parameters:
931         minimum - smallest id that may be returned
932         maximum - largest id that may be returned
933
934     Returns: the id, or None if there are none available
935
936     Example: first_available_id(20000, 40000) -> 20018
937     """
938
939     # get lists of used uids and gids in LDAP
940     uids = ldap_connection.used_uids(minimum, maximum)
941     gids = ldap_connection.used_gids(minimum, maximum)
942
943     # iterate through the lists and return the first available
944     for ugid in xrange(minimum, maximum+1):
945         if ugid not in uids and ugid not in gids and check_id_nss(ugid):
946             return ugid
947
948     # no id found within the range
949     return None
950
951
952
953 ### Tests ###
954
955 if __name__ == '__main__':
956
957     import random
958     from csc.common.test import *
959
960     def test_exists(name):
961         return ldap_connection.user_lookup(name) is not None, \
962             ldap_connection.group_lookup(name) is not None, \
963             krb_connection.get_principal(name) is not None
964
965     # t=test u=user m=member a=adminv c=club
966     # g=group r=real e=expected n=new
967     tuname = 'testuser'
968     turname = 'Test User'
969     tunrname = 'User Test'
970     tudesc = 'May be deleted'
971     tuhome = '/home/testuser'
972     tunhome = '/users/testuser'
973     tushell = '/bin/false'
974     tunshell = '/bin/true'
975     tugecos = 'Test User,,,'
976     tungecos = 'User Test,,,'
977     tmname = 'testmember'
978     tmrname = 'Test Member'
979     tmmid = 31415
980     tcname = 'testclub'
981     tcrname = 'Test Club'
982     tcmid = 98696
983     taname = 'testadm'
984     tarname = 'Test Adm' 
985     tgname = 'testgroup'
986     tgdesc = 'Test Group'
987     minid = 99999000
988     maxid = 100000000
989     tpw = str(random.randint(10**30, 10**31-1))
990     tgecos = 'a,b,c,d,e'
991     tgecos_args = 'a','b','c','d','e'
992
993     test(connect)
994     connect()
995     success()
996
997     try:
998         delete(tuname); delete(tmname)
999         delete(tcname); delete(taname)
1000         delete_group(tgname)
1001     except (NoSuchAccount, NoSuchGroup):
1002         pass
1003
1004     test(create)
1005     create(tuname, turname, minid, maxid, tuhome, tpw, tudesc, tugecos, tushell)
1006     exists = test_exists(tuname)
1007     expected = (True, True, True)
1008     assert_equal(expected, exists)
1009     success()
1010
1011     test(create_member)
1012     create_member(tmname, tpw, tmrname, tmmid)
1013     exists = test_exists(tmname)
1014     expected = (True, False, True)
1015     assert_equal(expected, exists)
1016     success()
1017
1018     test(create_club)
1019     create_club(tcname, tmrname, tmmid)
1020     exists = test_exists(tcname)
1021     expected = (True, False, False)
1022     assert_equal(expected, exists)
1023     success()
1024
1025     test(create_adm)
1026     create_adm(taname, tarname)
1027     exists = test_exists(taname)
1028     expected = (True, False, False)
1029     assert_equal(expected, exists)
1030     success()
1031
1032     test(create_group)
1033     create_group(tgname, minid, maxid, tgdesc)
1034     exists = test_exists(tgname)
1035     expected = (False, True, False)
1036     assert_equal(expected, exists)
1037     success()
1038
1039     test(status)
1040     assert_equal((True, True), status(tmname))
1041     assert_equal((True, False), status(tcname))
1042     success()
1043
1044     test(reset_password)
1045     reset_password(tuname, str(int(tpw)/2))
1046     reset_password(tmname, str(int(tpw)/3))
1047     negative(reset_password, (tcname,str(int(tpw)/4)), NoSuchAccount, "club should not have password")
1048     negative(reset_password, (taname,str(int(tpw)/5)), NoSuchAccount, "club should not have password")
1049     success()
1050
1051     test(get_uid)
1052     tuuid = get_uid(tuname)
1053     assert_equal(True, int(tuuid) >= 0)
1054     success()
1055
1056     test(get_gid)
1057     tugid = get_gid(tuname)
1058     assert_equal(True, int(tugid) >= 0)
1059     success()
1060
1061     test(get_gecos)
1062     ugecos = get_gecos(tuname)
1063     assert_equal(tugecos, ugecos)
1064     success()
1065
1066     test(update_gecos)
1067     update_gecos(tuname, tungecos)
1068     ugecos = get_gecos(tuname)
1069     assert_equal(tungecos, ugecos)
1070     success()
1071
1072     test(get_shell)
1073     ushell = get_shell(tuname)
1074     assert_equal(tushell, ushell)
1075     success()
1076
1077     test(update_shell)
1078     update_shell(tuname, tunshell, False)
1079     ushell = get_shell(tuname)
1080     assert_equal(ushell, tunshell)
1081     success()
1082
1083     test(get_name)
1084     urname = get_name(tuname)
1085     assert_equal(turname, urname)
1086     success()
1087
1088     test(update_name)
1089     update_name(tuname, tunrname)
1090     urname = get_name(tuname)
1091     assert_equal(urname, tunrname)
1092     success()
1093
1094     test(get_home)
1095     uhome = get_home(tuname)
1096     assert_equal(tuhome, uhome)
1097     success()
1098
1099     test(update_home)
1100     update_home(tuname, tunhome)
1101     urhome = get_home(tuname)
1102     assert_equal(urhome, tunhome)
1103     success()
1104
1105     test(get_members)
1106     members = get_members(tgname)
1107     expected = []
1108     assert_equal(expected, members)
1109     success()
1110
1111     test(check_membership)
1112     member = check_membership(tuname, tgname)
1113     assert_equal(False, member)
1114     member = check_membership(tuname, tuname)
1115     assert_equal(True, member)
1116     success()
1117
1118     test(add_member)
1119     add_member(tuname, tgname)
1120     assert_equal(True, check_membership(tuname, tgname))
1121     assert_equal([tuname], get_members(tgname))
1122     success()
1123
1124     test(remove_member)
1125     assert_equal(True, remove_member(tuname, tgname))
1126     assert_equal(False, check_membership(tuname, tgname))
1127     assert_equal(False, remove_member(tuname, tgname))
1128     success()
1129
1130     test(build_gecos)
1131     assert_equal(tgecos, build_gecos(*tgecos_args))
1132     success()
1133
1134     test(parse_gecos)
1135     gecos_dict = parse_gecos(tgecos)
1136     assert_equal(tgecos, build_gecos(**gecos_dict))
1137     success()
1138
1139     test(delete)
1140     delete(tuname)
1141     exists = test_exists(tuname)
1142     expected = (False, False, False)
1143     assert_equal(expected, exists)
1144     delete(tmname)
1145     exists = test_exists(tmname)
1146     assert_equal(expected, exists)
1147     delete(tcname)
1148     exists = test_exists(tcname)
1149     assert_equal(expected, exists)
1150     delete(taname)
1151     exists = test_exists(taname)
1152     assert_equal(expected, exists)
1153     success()
1154
1155     test(delete_group)
1156     delete_group(tgname)
1157     exists = test_exists(tgname)
1158     expected = (False, False, False)
1159     assert_equal(expected, exists)
1160     success()
1161
1162     test(disconnect)
1163     disconnect()
1164     success()