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