def getManagedGroups(conn, user): ''' Get the groups this user manages. user may be DN or username. Array of dn,attrs is returned for each group ''' groupdata = [] result = [] if user.upper().startswith('CN='): result = conn.search(distinguishedName=user) else: result = conn.search(sAMAccountName=user) if len(result) == 0: raise UserException('Unable to find user %s' % user) if len(result) > 1: raise UserException('Multiple users retrieved when searching with %s' % user) if 'managedObjects' in result[0][1]: for groupdn in result[0][1]['managedObjects']: groupdata = groupdata + conn.search(domain=ad.GROUP_DOMAIN, objectclass='Group', distinguishedName=groupdn) return groupdata
def groupnameToDn(conn, groupname): ''' Get the Distinguished Name (CN=schrag_lab,OU=EPS,OU=Domain Groups,DC=rc,DC=domain) for a group name (e.g. schrag_lab) ''' result = conn.search(domain=ad.GROUP_DOMAIN, objectclass='Group', sAMAccountName=groupname) if len(result) == 0: result = conn.search(domain=ad.AFFILIATIONS_DOMAIN, objectclass='Group', sAMAccountName=groupname) if len(result) == 0: result = conn.search(domain=ad.GROUP_DOMAIN, objectclass='Group', cn=groupname) if len(result) == 0: result = conn.search(domain=ad.GROUP_DOMAIN, objectclass='Group', name=groupname) if len(result) == 0: raise UserException('Unable to find group with name %s' % groupname) if len(result) > 1: raise UserException('Multiple groups with name %s' % groupname) return result[0][0]
def usernameToDn(conn, username): ''' Get the Distinguished Name associated with a username ''' result = conn.search(sAMAccountName=username) if len(result) == 0: raise UserException('Unable to find user with username %s' % username) if len(result) > 1: raise UserException('Multiple accounts with username %s' % username) return result[0][0]
def move(self, dn, ou): ''' Move the dn to ou. For example, move CN=John Noos,OU=_new_accounts,OU=RC,OU=Domain Users,DC=rc,DC=domain to CN=John Noss,OU=CloudOps,OU=RC,OU=Domain Users,DC=rc,DC=domain ''' elements = dn.split(',') if len(elements) < 2: raise UserException('%s does not look like a distinguishedName' % dn) if not elements[0].upper().startswith('CN='): raise UserException('Cannot move %s; It doesn\'t start with a CN' % dn) self.conn.rename_s(dn, elements[0], ou)
def initHomedir(home, username, groupname, skeldir=None): ''' Creates an initial home directory, copies skeleton if defined and makes a .ssh dir. Chowns the dir to the specified username:groupname ''' if os.system('id %s > /dev/null' % username) != 0: raise Exception('User %s does not exist' % username) if skeldir: shutil.copytree(skeldir, home) else: os.makedirs(home) os.chmod(home, DEFAULT_HOMEDIR_MODE) # Create the .ssh dir os.makedirs(os.path.join(home, '.ssh'), 0o700) logger.debug('Made %s' % os.path.join(home, '.ssh')) # Chown the contents to the user # This is run as a sudo command only for the sake of testing cmd = 'sudo chown -R %s:%s %s' % (username, groupname, home) logger.debug('Running cmd %s' % cmd) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() logger.debug('Cmd returns %d. %s' % (p.returncode, stderr)) if p.returncode != 0: raise UserException('chown of homedir failed: %s\n%s' % (' '.join(cmd), stderr))
def generateHomedirPath(username, groups): ''' Create a homedir path with a combination of the username and the listing of groups from the 'memberOf' list. If the user is part of a cluster user group, then DEFAULT_HOME_ROOT will be used along with the random homedir between home00-homeHOME_DIR_RANGE and the username If the user is an NCF user, the NCF_HOME_ROOT will be used. If the user is not NCF or in a cluster user group, an Exception will be thrown. ''' isclusteruser = False for cudn in CLUSTER_USERS_GROUP_DNS: if cudn in groups: isclusteruser = True # If it's an NCF user if NCF_USER_GROUP_DN in groups: return generateNcfHomedirPath(username) elif isclusteruser: return generateOdysseyHomedirPath(username) else: raise UserException( 'Cannot create a home directory for user that is not in one of the cluster user groups such as %s' % ' or '.join(CLUSTER_USERS_GROUP_DNS))
def delete(self, dn): ''' Remove the dn from the system ''' try: self.conn.delete_s(dn) except ldap.NO_SUCH_OBJECT: raise UserException('Unable to delete %s. No such object.' % dn)
def enableNewUser(conn, userdn, ou): ''' Activate a user that is in the _new_accounts OU by moving to the specified OU and calling enableUser ''' if not userdn.upper().startswith('CN=') or NEW_ACCOUNT_OU.upper( ) not in userdn.upper(): raise UserException( 'Cannot enable %s. Must specify a distinguished name that starts with CN= and includes the NEW_ACCOUNT_OU' % userdn) enableUser(conn, userdn) conn.move(userdn, ou)
def setupSshKey(username, home): ''' Create an ssh key for a user using ssh-keygen. Also appends key to authorized_keys ''' # Create public key cmd = "sudo su - %s -c \"ssh-keygen -qN '' -t rsa -f %s\"" % ( username, os.path.join(home, '.ssh', 'id_rsa')) logger.debug('Running cmd %s' % cmd) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() logger.debug('Cmd returns %d. %s' % (p.returncode, stderr)) if p.returncode != 0: raise UserException('Creation of public key failed: %s\n%s' % (' '.join(cmd), stderr)) # Add key to authorized keys cmd = 'sudo su - %s -c "cat %s >> %s"' % ( username, os.path.join(home, '.ssh', 'id_rsa.pub'), os.path.join(home, '.ssh', 'authorized_keys')) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: raise UserException( 'Copying public key to authorized_keys failed: %s\n%s' % (' '.join(cmd), stderr))
def addToInstrumentGroup(conn, user, group): ''' This is because the instrument groups are not under Domain Groups ''' groupdn = group if not groupdn.upper().startswith('CN='): result = conn.search(domain=ad.INSTRUMENT_DOMAIN, objectclass='Group', sAMAccountName=group) if len(result) == 0: raise UserException('Unable to find group with name %s' % group) groupdn = result[0][0] addToGroup(conn, user, groupdn)
def makeLabDirs(username, dirname=None): ''' Create the lab directories for the given PI username. By default the directory name is the PI users primary group ''' dirs = {} groupname = getUnixUserPrimaryGroup(username) if dirname is None: dirname = groupname dirs['shared_scratch'] = os.path.join(DEFAULT_SHARED_SCRATCH, dirname) for key, path in dirs.items(): try: os.makedirs(path) # Chown the contents to the user # This is run as a sudo command only for the sake of testing cmd = 'sudo chown -R %s:%s %s; chmod 2770 %s' % ( username, groupname, path, path) logger.debug('Running cmd %s' % cmd) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() logger.debug('Cmd returns %d. %s' % (p.returncode, stderr)) if p.returncode != 0: raise UserException('chown of homedir failed: %s\n%s' % (' '.join(cmd), stderr)) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(path): # If it already exists, skip it pass else: raise
def makeHomedir(conn, user, home=None, skeldir=None): ''' Pick out an appropriate home directory and make it. Has to be run from sa01. Can be either a username or a DN. Exceptions are thrown if - the specified user doesn't exist - the home directory that is selected already exists, an - the user has no uidNumber or gidNumber attribute - the user does not exist on the current system or has no group name - the user is not part of one of the cluster user groups (CLUSTER_USERS_GROUP_DNS) - any of the shell commands (copy the skel dir, chown the new home, create the ssh key) fails If any of the home directory setup steps fails, any partial directory creation is cleaned up and the old home value (i.e. /dev/null) is restored to the AD attribute ''' # Find the user userdn = user if not userdn.upper().startswith('CN='): userdn = usernameToDn(conn, user) userdata = conn.search(distinguishedName=userdn) if len(userdata) < 1: raise UserException('User %s does not exist in the system' % user) username = userdata[0][1]['sAMAccountName'][0] # Make sure she has a uidNumber and gidNumber if 'uidNumber' not in userdata[0][1] or len( userdata[0][1]['uidNumber']) == 0: raise UserException('User %s has no uid set.' % userdn) uid = int(userdata[0][1]['uidNumber'][0]) if 'gidNumber' not in userdata[0][1] or len( userdata[0][1]['gidNumber']) == 0: raise UserException('User %s has no primary group set.' % userdn) gid = int(userdata[0][1]['gidNumber'][0]) # Get the group name from the username. Needed later for chowning. groupname = getUnixUserPrimaryGroup(username) oldhome = userdata[0][1]['unixHomeDirectory'][0] # If home and skeldir are not explicitly set, either 1) use the homedir set in the attribute (if it's not /dev/null (DEFAULT_UNIX_HOME) # or 2) generate a new one if home is None and skeldir is None: if oldhome != DEFAULT_UNIX_HOME: # Must have been set by another process. We'll let it stand. home = oldhome else: home = generateHomedirPath(username, userdata[0][1]['memberOf']) # If it's an NCF user if NCF_USER_GROUP_DN in userdata[0][1]['memberOf']: skeldir = NCF_SKELDIR else: skeldir = DEFAULT_SKELDIR # Set the new home directory setHomedirAttribute(conn, userdn, home) # Flag used by the exception handler to determine if the newly create dir should be removed. removehome = False # Create the home directory try: # Bail if it already exists if os.path.exists(home): raise Exception('Home directory %s already exists.' % home) removehome = True initHomedir(home, username, groupname, skeldir) # Setup the ssh key setupSshKey(username, home) except Exception as e: # If directory creation fails, set back to the old one. conn.setAttributes(userdn, unixHomeDirectory=[oldhome]) if removehome and os.path.exists(home): shutil.rmtree(home, ignore_errors=True) raise e
def setPrimaryGroup(conn, user, pi=None, groupdn=None, gid=None): ''' For the given cluster user, set the primary group. nis domain (msSFU30NisDomain) is also set to DEFAULT_NIS_DOMAIN If groupdn and gid are set, those will be used directly. If pi is set, then we search for a managedObject associated with the PI. If multiple managedObjects are found, the one with _lab in it will be used. If neither pi nor groupdn/gid are set, the DEFAULT_PRIMARY_GROUP_DN and DEFAULT_PRIMARY_GROUP_GID will be used. ''' pgroupdn = '' pgroupgid = '' userdn = user if not userdn.upper().startswith('CN='): userdn = usernameToDn(conn, user) # If they are explicit, use them if groupdn is not None and gid is not None: pgroupdn = groupdn pgroupgid = str(gid) # If the PI is defined, get her managedObjects. # If there is only 1, use it. # If there are multiple, pick the one that says _lab # If there are multiple _labs, give up elif pi is not None: pidn = pi if not pidn.upper().startswith('CN='): pidn = usernameToDn(conn, pi) pidata = conn.search(distinguishedName=pidn) if pidata is None or len(pidata) == 0 or len(pidata[0]) == 0: raise Exception('Unable to locate PI %s' % pidn) # Special processing for hepl if 'memberOf' in pidata[0][1] and HEPL_DN in pidata[0][1]['memberOf']: pgroupdn = HEPL_DN pgroupgid = HEPL_GID # Special processing for hetg elif 'memberOf' in pidata[0][1] and HETG_DN in pidata[0][1]['memberOf']: pgroupdn = HETG_DN pgroupgid = HETG_GID else: pigroups = getPiLabGroups(conn, pidn) if pigroups is None: raise UserException('PI %s does not manage any groups.' % pi) if len(pigroups) > 1: raise UserException( 'PI %s manages multiple labs and I cannot figure out which one you want. Run setPrimaryGroup with the desired groupdn and gid.' % pi) pgroupdn = pigroups[0][0] pgroupgid = pigroups[0][1] else: pgroupdn = DEFAULT_PRIMARY_GROUP_DN pgroupgid = DEFAULT_PRIMARY_GROUP_ID # Not sure what this Claire Reardon stuff is about # if user_dn == 'cn=%s,%s' % ('Claire Reardon', NEW_ACCOUNT_OU): # gidNumber = '402738' # group_dn = 'CN=external_users,OU=External,OU=Domain Groups,DC=rc,DC=domain' conn.setAttributes(userdn, msSFU30NisDomain=DEFAULT_NIS_DOMAIN, gidNumber=pgroupgid) conn.addUsersToGroups(userdn, pgroupdn) return (pgroupgid, pgroupdn)
def addNewAccount(conn, **kwargs): ''' Add a new account to AD. keyword args must include cn, username, mail, department A list of groups (by sAMAAccountName), title, unix home, etc. may also be included. Returns the DN of the new account ''' # Check required fields for key in REQUIREDATTRS: if key not in kwargs or kwargs[key] is None or kwargs[key].strip( ) == '': raise UserException( 'Required field %s missing. New accounts must have %s.' % (key, ','.join(REQUIREDATTRS))) # Pop the requireds. Everything else will be added as is # cn, first name and last name (see below) are popped, stripped, unicoded, then normalized. cn = cleanADName(kwargs.pop('cn')) username = cleanADName( kwargs.pop('username')) # python ldap doesn't like unicode here mail = cleanADName(kwargs.pop('mail')) department = ldap.filter.filter_format( '%s', [str(kwargs.pop('department').strip())]) phone = cleanADName(kwargs.pop('telephoneNumber')) title = str(kwargs.pop('title').strip()) # Get first name, last name. There may only be a first name. names = re.split(r'\s+', cn, 1) firstname = lastname = '' if len(names) > 1: firstname = names[0] lastname = names[1] else: lastname = names[0] # If givenName and sn are defined, use those: if 'givenName' in kwargs: firstname = cleanADName(kwargs.pop('givenName')) if 'sn' in kwargs: lastname = cleanADName(kwargs.pop('sn')) # Does the email address exist in the system? result = conn.search(mail=mail) if len(result) > 0: raise UserException( 'Email %s already exists in the system for user %s.' % (mail, result[0][0])) # Does the username exist in the system? result = conn.search(sAMAccountName=username) if len(result) > 0: raise UserException( 'Username %s already exists in the system for user %s' % (username, result[0][0])) # Non alphanumerics in username is a no-no if re.search(r'[^A-Za-z0-9]', username) is not None: raise UserException('Username may only contain A-Za-z0-9') # No numbers in names for some reason? # if re.search(r'\d',cn) is not None: # raise UserException('cn (common name) cannot contain numbers') OU = NEW_ACCOUNT_OU if 'ou' in kwargs.keys(): OU = kwargs.pop('ou').strip() # The dn of our new entry/object dn = 'CN=%s,%s' % (cn, OU) # Set displayname displayname = ' '.join(names) # Set the uidnumber uidnumber = str(getNextUid(conn) + 1) upn = '%s@%s' % (username, ad.DOMAIN_STRING) dept = department[:63] attrs = [ ('objectclass', [b'top', b'person', b'organizationalperson', b'user']), ('userPrincipalName', [upn.encode('utf-8')]), ('uid', [username.encode('utf-8')]), ('uidNumber', [uidnumber.encode('utf-8')]), ('distinguishedName', [dn.encode('utf-8')]), ('sAMAccountName', [username.encode('utf-8')]), ('displayName', [displayname.encode('utf-8')]), ('mail', [mail.encode('utf-8')]), ('telephoneNumber', [phone.encode('utf-8')]), ('title', [title.encode('utf-8')]), ('description', [title.encode('utf-8')]), ('department', [dept.encode('utf-8')]), ('loginShell', [DEFAULT_LOGIN_SHELL.encode('utf-8')]), ('unixHomeDirectory', [DEFAULT_UNIX_HOME.encode('utf-8')]), ('msSFU30NisDomain', [b'<none>']), ('msSFU30Name', [username.encode('utf-8')]), ('ou', [OU.encode('utf-8')]), ('userAccountControl', [b'514']), ('pwdLastSet', [b'-1']), ] if firstname != '': givenName = ldap.filter.filter_format('%s', [firstname]) attrs.append(('givenName', [givenName.encode('utf-8')])) if lastname != '': sn = ldap.filter.filter_format('%s', [lastname]) attrs.append(('sn', [sn.encode('utf-8')])) # Add all remaining attributes as is # If one of the kwargs is 'password', set it. # Also hang on to the expiration date password = None expirationdate = None requirepasswordreset = True for k, v in kwargs.items(): if not isinstance(v, list): if isinstance(v, str): v = v.encode('utf-8') v = [v] if k == 'password': password = ldap.filter.filter_format('%s', [v[0]]) elif k == 'expirationDate': expirationdate = v[0] elif k == 'requirePasswordReset': if not v[0]: requirepasswordreset = False else: attrs.append((k, v)) # Add the user try: conn.add(dn, attrs) except Exception as e: logger.exception(e) if 'desc' in e and 'Already exists' in e['desc']: raise Exception('User %s %s with email %s already exists' % (cn, username, mail)) else: raise # If there are any failures at this point, remove the account if possible try: # Set password if password is not None: conn.setPassword(dn, password) # Set expiration date if expirationdate is not None: setExpirationDate(conn, dn, expirationdate) # Require password reset if requirepasswordreset: requirePasswordReset(conn, dn) except Exception as e: logger.error( 'Account removed due to error after creating account for %s: %s\n%s' % (dn, str(e), traceback.format_exc())) conn.delete(dn) raise e return dn