Reorganize build process
[mspang/pyceo.git] / pylib / csc / adm / members.py
index 27ff083..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
-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_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."""
@@ -61,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 ###
@@ -71,11 +82,10 @@ ldap_connection = ldapi.LDAPConnection()
 def connect():
     """Connect to LDAP."""
 
-    load_configuration()
+    configure()
+
     ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
-        cfg['sasl_realm'], cfg['admin_bind_userid'],
-        ('keytab', cfg['admin_bind_keytab']), cfg['users_base'],
-        cfg['groups_base'])
+        cfg['sasl_realm'], cfg['users_base'], cfg['groups_base'])
 
 def disconnect():
     """Disconnect from LDAP."""
@@ -92,6 +102,41 @@ def connected():
 
 ### Members ###
 
+def create_member(username, password, name, program):
+    """
+    Creates a UNIX user account with options tailored to CSC members.
+
+    Parameters:
+        username - the desired UNIX username
+        password - the desired UNIX password
+        name     - the member's real name
+        program  - the member's program of study
+
+    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']))
+
+    # 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'])
+
+    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()
+
+    if status:
+        raise ChildFailed("addmember", status, out+err)
+
+
 def get(userid):
     """
     Look up attributes of a member by userid.
@@ -133,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', ... },
@@ -256,6 +300,39 @@ def change_group_member(action, group, userid):
     ceo_ldap.modify_s(group_dn, mlist)
 
 
+
+### 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):
@@ -274,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 ]
 
@@ -284,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):