Add mysql database stuff
[mspang/pyceo.git] / ceo / members.py
index 83890e8..5ebf272 100644 (file)
@@ -9,8 +9,8 @@ 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 os, re, subprocess, ldap
-from ceo import conf, ldapi
+import os, re, subprocess, ldap, socket
+from ceo import conf, ldapi, terms, remote, ceo_pb2
 from ceo.excep import InvalidArgument
 
 
@@ -23,10 +23,9 @@ cfg = {}
 def configure():
     """Load Members Configuration"""
 
-    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' ]
+    string_fields = [ 'username_regex', 'shells_file', 'ldap_server_url',
+            'ldap_users_base', 'ldap_groups_base', 'ldap_sasl_mech', 'ldap_sasl_realm',
+            'expire_hook', 'mathsoc_regex', 'mathsoc_dont_count' ]
     numeric_fields = [ 'min_password_length' ]
 
     # read configuration file
@@ -67,16 +66,6 @@ class NoSuchMember(MemberException):
     def __str__(self):
         return "Member not found: %d" % self.memberid
 
-class ChildFailed(MemberException):
-    def __init__(self, program, status, output):
-        MemberException.__init__(self)
-        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 ###
 
@@ -86,15 +75,14 @@ ld = None
 def connect(auth_callback):
     """Connect to LDAP."""
 
-    configure()
 
     global ld
     password = None
     tries = 0
     while ld is None:
         try:
-            ld = ldapi.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
-                cfg['sasl_realm'], password)
+            ld = ldapi.connect_sasl(cfg['ldap_server_url'], cfg['ldap_sasl_mech'],
+                cfg['ldap_sasl_realm'], password)
         except ldap.LOCAL_ERROR, e:
             tries += 1
             if tries > 3:
@@ -103,6 +91,11 @@ def connect(auth_callback):
             if password == None:
                 raise e
 
+def connect_anonymous():
+    """Connect to LDAP."""
+
+    global ld
+    ld = ldap.initialize(cfg['ldap_server_url'])
 
 def disconnect():
     """Disconnect from LDAP."""
@@ -121,7 +114,7 @@ def connected():
 
 ### Members ###
 
-def create_member(username, password, name, program):
+def create_member(username, password, name, program, email):
     """
     Creates a UNIX user account with options tailored to CSC members.
 
@@ -130,6 +123,7 @@ def create_member(username, password, name, program):
         password - the desired UNIX password
         name     - the member's real name
         program  - the member's program of study
+       email    - email to place in .forward
 
     Exceptions:
         InvalidArgument - on bad account attributes provided
@@ -148,15 +142,79 @@ def create_member(username, password, name, program):
         raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
 
     try:
-        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()
+        request = ceo_pb2.AddUser()
+        request.type = ceo_pb2.AddUser.MEMBER
+        request.username = username
+        request.password = password
+        request.realname = name
+        request.program = program
+       request.email = email
+
+        out = remote.run_remote('adduser', request.SerializeToString())
+
+        response = ceo_pb2.AddUserResponse()
+        response.ParseFromString(out)
+
+        if any(message.status != 0 for message in response.messages):
+            raise MemberException('\n'.join(message.message for message in response.messages))
+
+        # # If the user was created, consider adding them to the mailing list
+        # if not status:
+        #     listadmin_cfg_file = "/path/to/the/listadmin/config/file"
+        #     mail = subprocess.Popen(["/usr/bin/listadmin", "-f", listadmin_cfg_file, "--add-member", username + "@csclub.uwaterloo.ca"])
+        #     status2 = mail.wait() # Fuck if I care about errors!
+    except remote.RemoteException, e:
+        raise MemberException(e)
     except OSError, e:
         raise MemberException(e)
 
-    if status:
-        raise ChildFailed("addmember", status, out+err)
+
+def check_email(email):
+    match = re.match('^\S+?@(\S+)$', email)
+    if not match:
+        return 'Invalid email address'
+
+    # some characters are treated specially in .forward
+    for c in email:
+        if c in ('"', "'", ',', '|', '$', '/', '#', ':'):
+            return 'Invalid character in address: %s' % c
+
+    host = match.group(1)
+    try:
+        ip = socket.gethostbyname(host)
+    except:
+        return 'Invalid host: %s' % host
+
+
+def current_email(username):
+    fwdpath = '%s/%s/.forward' % (cfg['member_home'], username)
+    try:
+        fwd = open(fwdpath).read().strip()
+        if not check_email(fwd):
+            return fwd
+    except OSError:
+        pass
+    except IOError:
+        pass
+
+
+def change_email(username, forward):
+    try:
+        request = ceo_pb2.UpdateMail()
+        request.username = username
+        request.forward = forward
+
+        out = remote.run_remote('mail', request.SerializeToString())
+
+        response = ceo_pb2.AddUserResponse()
+        response.ParseFromString(out)
+
+        if any(message.status != 0 for message in response.messages):
+            return '\n'.join(message.message for message in response.messages)
+    except remote.RemoteException, e:
+        raise MemberException(e)
+    except OSError, e:
+        raise MemberException(e)
 
 
 def get(userid):
@@ -172,7 +230,10 @@ def get(userid):
              }
     """
 
-    return ldapi.lookup(ld, 'uid', userid, cfg['users_base'])
+    return ldapi.lookup(ld, 'uid', userid, cfg['ldap_users_base'])
+
+def uid2dn(uid):
+    return 'uid=%s,%s' % (ldapi.escape(uid), cfg['ldap_users_base'])
 
 
 def list_term(term):
@@ -185,16 +246,15 @@ def list_term(term):
     Returns: a list of members
 
     Example: list_term('f2006'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
-                 'ctdalek': { 'cn': 'Calum T. Dalek', ... },
+                 'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
+                 'uid=ctdalek, ou=...': { 'cn': 'Calum T. Dalek', ... },
                  ...
              }
     """
 
-    members = ldapi.search(ld, cfg['users_base'],
+    members = ldapi.search(ld, cfg['ldap_users_base'],
             '(&(objectClass=member)(term=%s))', [ term ])
-
-    return dict([(member[1]['uid'][0], member[1]) for member in members])
+    return dict([(member[0], member[1]) for member in members])
 
 
 def list_name(name):
@@ -207,15 +267,14 @@ def list_name(name):
     Returns: a list of member dictionaries
 
     Example: list_name('Spang'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
+                 'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
                  ...
              ]
     """
 
-    members = ldapi.search(ld, cfg['users_base'],
+    members = ldapi.search(ld, cfg['ldap_users_base'],
             '(&(objectClass=member)(cn~=%s))', [ name ])
-
-    return dict([(member[1]['uid'][0], member[1]) for member in members])
+    return dict([(member[0], member[1]) for member in members])
 
 
 def list_group(group):
@@ -228,7 +287,7 @@ def list_group(group):
     Returns: a list of member dictionaries
 
     Example: list_name('syscom'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
+                 'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
                  ...
              ]
     """
@@ -239,10 +298,26 @@ def list_group(group):
         for member in members:
             info = get(member)
             if info:
-                ret[member] = info
+                ret[uid2dn(member)] = info
     return ret
 
 
+def list_all():
+    """
+    Build a list of all members
+
+    Returns: a list of member dictionaries
+
+    Example: list_name('Spang'): -> {
+                 'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
+                 ...
+             ]
+    """
+
+    members = ldapi.search(ld, cfg['ldap_users_base'], '(objectClass=member)')
+    return dict([(member[0], member[1]) for member in members])
+
+
 def list_positions():
     """
     Build a list of positions
@@ -255,7 +330,7 @@ def list_positions():
              ]
     """
 
-    members = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
+    members = ld.search_s(cfg['ldap_users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
     positions = {}
     for (_, member) in members:
         for position in member['position']:
@@ -276,7 +351,7 @@ def set_position(position, members):
     Example: set_position('president', ['dtbartle'])
     """
 
-    res = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE,
+    res = ld.search_s(cfg['ldap_users_base'], ldap.SCOPE_SUBTREE,
         '(&(objectClass=member)(position=%s))' % ldapi.escape(position))
     old = set([ member['uid'][0] for (_, member) in res ])
     new = set(members)
@@ -289,7 +364,7 @@ def set_position(position, members):
 
     for action in ['del', 'add']:
         for userid in mods[action]:
-            dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
+            dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
             entry1 = {'position' : [position]}
             entry2 = {} #{'position' : []}
             entry = ()
@@ -302,8 +377,8 @@ def set_position(position, members):
 
 
 def change_group_member(action, group, userid):
-    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
-    group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['groups_base'])
+    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
+    group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['ldap_groups_base'])
     entry1 = {'uniqueMember' : []}
     entry2 = {'uniqueMember' : [user_dn]}
     entry = []
@@ -321,7 +396,7 @@ def change_group_member(action, group, userid):
 ### Shells ###
 
 def get_shell(userid):
-    member = ldapi.lookup(ld, 'uid', userid, cfg['users_base'])
+    member = ldapi.lookup(ld, 'uid', userid, cfg['ldap_users_base'])
     if not member:
         raise NoSuchMember(userid)
     if 'loginShell' not in member:
@@ -340,7 +415,7 @@ def get_shells():
 def set_shell(userid, shell):
     if not shell in get_shells():
         raise InvalidArgument("shell", shell, "is not in %s" % cfg['shells_file'])
-    ldapi.modify(ld, 'uid', userid, cfg['users_base'], [ (ldap.MOD_REPLACE, 'loginShell', [ shell ]) ])
+    ldapi.modify(ld, 'uid', userid, cfg['ldap_users_base'], [ (ldap.MOD_REPLACE, 'loginShell', [ shell ]) ])
 
 
 
@@ -367,16 +442,23 @@ def create_club(username, name):
         raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
     
     try:
-        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()
+        request = ceo_pb2.AddUser()
+        request.type = ceo_pb2.AddUser.CLUB
+        request.username = username
+        request.realname = name
+
+        out = remote.run_remote('adduser', request.SerializeToString())
+
+        response = ceo_pb2.AddUserResponse()
+        response.ParseFromString(out)
+
+        if any(message.status != 0 for message in response.messages):
+            raise MemberException('\n'.join(message.message for message in response.messages))
+    except remote.RemoteException, e:
+        raise MemberException(e)
     except OSError, e:
         raise MemberException(e)
 
-    if status:
-        raise ChildFailed("addclub", status, out+err)
-
 
 
 ### Terms ###
@@ -397,7 +479,7 @@ def register(userid, term_list):
     Example: register(3349, ["w2007", "s2007"])
     """
 
-    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
+    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
 
     if type(term_list) in (str, unicode):
         term_list = [ term_list ]
@@ -429,7 +511,7 @@ def register(userid, term_list):
 def register_nonmember(userid, term_list):
     """Registers a non-member for one or more terms."""
 
-    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
+    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
 
     if type(term_list) in (str, unicode):
         term_list = [ term_list ]
@@ -476,7 +558,10 @@ def registered(userid, term):
     """
 
     member = get(userid)
-    return 'term' in member and term in member['term']
+    if not member is None:
+        return 'term' in member and term in member['term']
+    else:
+        return False
 
 
 def group_members(group):
@@ -485,15 +570,19 @@ def group_members(group):
     Returns a list of group members
     """
 
-    group = ldapi.lookup(ld, 'cn', group, cfg['groups_base'])
+    group = ldapi.lookup(ld, 'cn', group, cfg['ldap_groups_base'])
 
-    if group:
-        if 'uniqueMember' in group:
-            r = re.compile('^uid=([^,]*)')
-            return map(lambda x: r.match(x).group(1), group['uniqueMember'])
-        elif 'memberUid' in group:
-            return group['memberUid']
-        else:
-            return []
-    else:
-        return []
+    if group and 'uniqueMember' in group:
+        r = re.compile('^uid=([^,]*)')
+        return map(lambda x: r.match(x).group(1), group['uniqueMember'])
+    return []
+
+def expired_accounts():
+    members = ldapi.search(ld, cfg['ldap_users_base'],
+        '(&(objectClass=member)(!(|(term=%s)(nonMemberTerm=%s))))' %
+        (terms.current(), terms.current()))
+    return dict([(member[0], member[1]) for member in members])
+
+def send_account_expired_email(name, email):
+    args = [ cfg['expire_hook'], name, email ]
+    os.spawnv(os.P_WAIT, cfg['expire_hook'], args)