Reorganize build process
[mspang/pyceo.git] / pylib / csc / adm / members.py
index 0c1821f..bdf5bac 100644 (file)
@@ -9,31 +9,33 @@ Transactions are used in each method that modifies the database.
 Future changes to the members database that need to be atomic
 must also be moved into this module.
 """
-import re, ldap, os, pwd
-from csc.adm import terms
-from csc.backends import ldapi
+import re, subprocess, ldap
 from csc.common import conf
 from csc.common.excep import InvalidArgument
+from csc.backends import ldapi
 
 
 ### Configuration ###
 
-CONFIG_FILE = '/etc/csc/members.cf'
+CONFIG_FILE = '/etc/csc/accounts.cf'
 
 cfg = {}
 
-def load_configuration():
+def configure():
     """Load Members Configuration"""
 
-    string_fields = [ 'realname_regex', 'server_url', 'users_base',
-            'groups_base', 'sasl_mech', 'sasl_realm', 'admin_bind_dn',
-            'admin_bind_keytab', 'admin_bind_userid' ]
+    string_fields = [ 'username_regex', 'shells_file', 'server_url',
+            'users_base', 'groups_base', 'sasl_mech', 'sasl_realm',
+            'admin_bind_keytab', 'admin_bind_userid', 'realm',
+            'admin_principal', 'admin_keytab' ]
+    numeric_fields = [ 'min_password_length' ]
 
     # read configuration file
     cfg_tmp = conf.read(CONFIG_FILE)
 
     # verify configuration
     conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
+    conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
 
     # update the current configuration with the loaded values
     cfg.update(cfg_tmp)
@@ -43,6 +45,7 @@ def load_configuration():
 ### Exceptions ###
 
 ConfigurationException = conf.ConfigurationException
+LDAPException = ldapi.LDAPException
 
 class MemberException(Exception):
     """Base exception class for member-related errors."""
@@ -54,13 +57,6 @@ class InvalidTerm(MemberException):
     def __str__(self):
         return "Term is invalid: %s" % self.term
 
-class InvalidRealName(MemberException):
-    """Exception class for invalid real names."""
-    def __init__(self, name):
-        self.name = name
-    def __str__(self):
-        return "Name is invalid: %s" % self.name
-
 class NoSuchMember(MemberException):
     """Exception class for nonexistent members."""
     def __init__(self, memberid):
@@ -68,6 +64,14 @@ class NoSuchMember(MemberException):
     def __str__(self):
         return "Member not found: %d" % self.memberid
 
+class ChildFailed(MemberException):
+    def __init__(self, program, status, output):
+        self.program, self.status, self.output = program, status, output
+    def __str__(self):
+        msg = '%s failed with status %d' % (self.program, self.status)
+        if self.output:
+            msg += ': %s' % self.output
+        return msg
 
 
 ### Connection Management ###
@@ -78,11 +82,10 @@ ldap_connection = ldapi.LDAPConnection()
 def connect():
     """Connect to LDAP."""
 
-    load_configuration()
-    ldap_connection.connect_sasl(cfg['server_url'], cfg['admin_bind_dn'],
-        cfg['sasl_mech'], cfg['sasl_realm'], cfg['admin_bind_userid'],
-        ('keytab', cfg['admin_bind_keytab']), cfg['users_base'],
-        cfg['groups_base'])
+    configure()
+
+    ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
+        cfg['sasl_realm'], cfg['users_base'], cfg['groups_base'])
 
 def disconnect():
     """Disconnect from LDAP."""
@@ -97,49 +100,41 @@ def connected():
 
 
 
-### Member Table ###
+### Members ###
 
-def new(uid, realname, program=None):
+def create_member(username, password, name, program):
     """
-    Registers a new CSC member. The member is added to the members table
-    and registered for the current term.
+    Creates a UNIX user account with options tailored to CSC members.
 
     Parameters:
-        uid       - the initial user id
-        realname  - the full real name of the member
-        program   - the program of study of the member
-
-    Returns: the username of the new member
+        username - the desired UNIX username
+        password - the desired UNIX password
+        name     - the member's real name
+        program  - the member's program of study
 
     Exceptions:
-        InvalidRealName    - if the real name is malformed
-
-    Example: new("Michael Spang", program="CS") -> "mspang"
-    """
-
-    # blank attributes should be NULL
-    if program == '': program = None
-    if uid == '': uid = None
+        InvalidArgument - on bad account attributes provided
 
+    Returns: the uid number of the new account
 
-    # check real name format (UNIX account real names must not contain [,:=])
-    if not re.match(cfg['realname_regex'], realname):
-        raise InvalidRealName(realname)
+    See: create()
+    """
 
-    # check for duplicate userid
-    member = ldap_connection.user_lookup(uid)
-    if member:
-        raise InvalidArgument("uid", uid, "duplicate uid")
+    # check username format
+    if not username or not re.match(cfg['username_regex'], username):
+        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
 
-    # add the member to the directory
-    ldap_connection.member_add(uid, realname, program)
+    # check password length
+    if not password or len(password) < cfg['min_password_length']:
+        raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
 
-    # register them for this term in the directory
-    member = ldap_connection.member_lookup(uid)
-    member['term'] = [ terms.current() ]
-    ldap_connection.user_modify(uid, member)
+    args = [ "/usr/bin/addmember", "--stdin", username, name, program ]
+    addmember = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = addmember.communicate(password)
+    status = addmember.wait()
 
-    return uid
+    if status:
+        raise ChildFailed("addmember", status, out+err)
 
 
 def get(userid):
@@ -183,8 +178,7 @@ def list_name(name):
 
     Parameters:
         name - the name to match members against
-
-    Returns: a list of member dictionaries
+Returns: a list of member dictionaries
 
     Example: list_name('Spang'): -> {
                  'mspang': { 'cn': 'Michael Spang', ... },
@@ -234,9 +228,6 @@ def list_positions():
 
     ceo_ldap = ldap_connection.ldap
     user_base = ldap_connection.user_base
-    escape = ldap_connection.escape
-
-    if not ldap_connection.connected(): ldap_connection.connect()
 
     members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
     positions = {}
@@ -273,46 +264,19 @@ def set_position(position, members):
     if len(mods['del']) == 0 and len(mods['add']) == 0:
         return
 
-    for type in ['del', 'add']:
-        for userid in mods[type]:
+    for action in ['del', 'add']:
+        for userid in mods[action]:
             dn = 'uid=%s,%s' % (escape(userid), user_base)
             entry1 = {'position' : [position]}
             entry2 = {} #{'position' : []}
             entry = ()
-            if type == 'del':
+            if action == 'del':
                 entry = (entry1, entry2)
-            elif type == 'add':
+            elif action == 'add':
                 entry = (entry2, entry1)
             mlist = ldap_connection.make_modlist(entry[0], entry[1])
             ceo_ldap.modify_s(dn, mlist)
 
-def delete(userid):
-    """
-    Erase all records of a member.
-
-    Note: real members are never removed from the database
-
-    Returns: ldap entry of the member
-
-    Exceptions:
-        NoSuchMember - if the user id does not exist
-
-    Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
-    """
-
-    # save member data
-    member = ldap_connection.user_lookup(userid)
-
-    # bail if not found
-    if not member:
-        raise NoSuchMember(userid)
-
-    # remove data from the directory
-    uid = member['uid'][0]
-    ldap_connection.user_delete(uid)
-
-    return member
-
 
 def change_group_member(action, group, userid):
 
@@ -336,7 +300,40 @@ def change_group_member(action, group, userid):
     ceo_ldap.modify_s(group_dn, mlist)
 
 
-### Term Table ###
+
+### Clubs ###
+
+def create_club(username, name):
+    """
+    Creates a UNIX user account with options tailored to CSC-hosted clubs.
+    
+    Parameters:
+        username - the desired UNIX username
+        name     - the club name
+
+    Exceptions:
+        InvalidArgument - on bad account attributes provided
+
+    Returns: the uid number of the new account
+
+    See: create()
+    """
+
+    # check username format
+    if not username or not re.match(cfg['username_regex'], username):
+        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
+    
+    args = [ "/usr/bin/addclub", username, name ]
+    addclub = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = addclub.communicate()
+    status = addclub.wait()
+
+    if status:
+        raise ChildFailed("addclub", status, out+err)
+
+
+
+### Terms ###
 
 def register(userid, term_list):
     """
@@ -354,6 +351,11 @@ def register(userid, term_list):
     Example: register(3349, ["w2007", "s2007"])
     """
 
+    ceo_ldap = ldap_connection.ldap
+    user_base = ldap_connection.user_base
+    escape = ldap_connection.escape
+    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
+
     if type(term_list) in (str, unicode):
         term_list = [ term_list ]
 
@@ -364,16 +366,21 @@ def register(userid, term_list):
     if not ldap_member:
         raise NoSuchMember(userid)
 
+    new_member = ldap_member.copy()
+    new_member['term'] = new_member['term'][:]
+
     for term in term_list:
 
         # check term syntax
         if not re.match('^[wsf][0-9]{4}$', term):
             raise InvalidTerm(term)
 
-        # add the term to the directory
-        ldap_member['term'].append(term)
+        # add the term to the entry
+        if not term in ldap_member['term']:
+            new_member['term'].append(term)
 
-    ldap_connection.user_modify(userid, ldap_member)
+    mlist = ldap_connection.make_modlist(ldap_member, new_member)
+    ceo_ldap.modify_s(user_dn, mlist)
 
 
 def registered(userid, term):
@@ -394,25 +401,6 @@ def registered(userid, term):
     return 'term' in member and term in member['term']
 
 
-def member_terms(userid):
-    """
-    Retrieves a list of terms a member is
-    registered for.
-
-    Parameters:
-        userid - the member's username
-
-    Returns: list of term strings
-
-    Example: registered('ctdalek') -> 's1993'
-    """
-
-    member = ldap_connection.member_lookup(userid)
-    if not 'term' in member:
-        return []
-    else:
-        return member['term']
-
 def group_members(group):
 
     """
@@ -430,96 +418,3 @@ def group_members(group):
             return []
     else:
         return []
-
-
-### Tests ###
-
-if __name__ == '__main__':
-
-    from csc.common.test import *
-
-    # t=test m=member u=updated
-    tmname = 'Test Member'
-    tmuid = 'testmember'
-    tmprogram = 'Metaphysics'
-    tm2name = 'Test Member 2'
-    tm2uid = 'testmember2'
-    tm2uname = 'Test Member II'
-    tm2uprogram = 'Pseudoscience'
-
-    tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram] }
-    tm2dict = {'cn': [tm2name], 'uid': [tm2uid] }
-    tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram] }
-
-    thisterm = terms.current()
-    nextterm = terms.next(thisterm)
-
-    test(connect)
-    connect()
-    success()
-
-    test(connected)
-    assert_equal(True, connected())
-    success()
-
-    test(new)
-    tmid = new(tmuid, tmname, tmprogram)
-    tm2id = new(tm2uid, tm2name)
-    success()
-
-    test(registered)
-    assert_equal(True, registered(tmid, thisterm))
-    assert_equal(True, registered(tm2id, thisterm))
-    assert_equal(False, registered(tmid, nextterm))
-    success()
-
-    test(get)
-    tmp = get(tmid)
-    del tmp['objectClass']
-    del tmp['term']
-    assert_equal(tmdict, tmp)
-    tmp = get(tm2id)
-    del tmp['objectClass']
-    del tmp['term']
-    assert_equal(tm2dict, tmp)
-    success()
-
-    test(list_name)
-    assert_equal(True, tmid in list_name(tmname).keys())
-    assert_equal(True, tm2id in list_name(tm2name).keys())
-    success()
-
-    test(register)
-    register(tmid, nextterm)
-    assert_equal(True, registered(tmid, nextterm))
-    success()
-
-    test(member_terms)
-    assert_equal([thisterm, nextterm], member_terms(tmid))
-    assert_equal([thisterm], member_terms(tm2id))
-    success()
-
-    test(list_term)
-    assert_equal(True, tmid in list_term(thisterm).keys())
-    assert_equal(True, tmid in list_term(nextterm).keys())
-    assert_equal(True, tm2id in list_term(thisterm).keys())
-    assert_equal(False, tm2id in list_term(nextterm).keys())
-    success()
-
-    test(get)
-    tmp = get(tm2id)
-    del tmp['objectClass']
-    del tmp['term']
-    assert_equal(tm2dict, tmp)
-    success()
-
-    test(delete)
-    delete(tmid)
-    delete(tm2id)
-    success()
-
-    test(disconnect)
-    disconnect()
-    assert_equal(False, connected())
-    disconnect()
-    success()