#!/usr/bin/python # imports of modules import ConfigParser import optparse import os import re import random import string import subprocess import sys from datetime import datetime # defining some constants MOUNT_PATH = os.path.join("/mnt", "nfs-data-store-1", "drive") REPO_PATH = os.path.join(MOUNT_PATH, "svn-repository") AUTHZ_PATH = os.path.join(REPO_PATH, "authz") HTPWD_PATH = os.path.join(REPO_PATH, ".htpasswd") ADMINS = "administrators" USERS = "users" RESTRICTED = "restricted" ALUMNI = "alumni" NO_ACL = "" READ_ACL = "r" WRITE_ACL = "rw" GROUP_DEFAULTS = { ADMINS: WRITE_ACL, USERS: READ_ACL, RESTRICTED: NO_ACL, ALUMNI: NO_ACL } SVN_SUFFIX = ":/" re_separators = re.compile("[\t ,;]+") def set_new_password(name, length=10): """ sets a new password for a username """ characters = string.ascii_letters + string.digits password = "".join(random.choice(characters) for i in range(length)) subprocess.check_call(["htpasswd", "-b", HTPWD_PATH, name, password]) return password def delete_password(name): """ deletes a password for a username """ # if the user was not added to the password db, the removal will show # an error message that is confusing to the user - at least it confused me # so redirect this to /dev/null with open(os.devnull, 'wb') as devnull: subprocess.check_call(["htpasswd", "-D", HTPWD_PATH, name], stderr=devnull) def create_new_repository(name): """ creates a repository for a user and checks in some stuff to get started """ # change the working directory to the sshfs mount point os.chdir(MOUNT_PATH) # create the new repository new_repo = os.path.join(REPO_PATH, name) subprocess.check_call(["svnadmin", "create", new_repo], stderr=subprocess.STDOUT) # check out a temporary working copy subprocess.check_call(["svn", "checkout", "file://" + new_repo, name]) # create subfolders today = datetime.now() year = "%04d" % today.year os.mkdir(os.path.join(name, year)) for month in range(today.month, 13): month_path = os.path.join(name, year, "%02d" % month) os.mkdir(month_path) subprocess.check_call(["touch", os.path.join(month_path, ".empty")]) # copy some examples for temp in ("experiment", "synthesis", "toc"): filename = "template-%s.doc" % temp in_file = os.path.join(REPO_PATH, filename) out_file = os.path.join(name, filename) subprocess.check_call(["cp", in_file, out_file]) # add and commit the changes subprocess.check_call("svn add %s/*" % name, shell=True) subprocess.check_call(["svn", "commit", "-m", "New User: " + name, name]) # remove the temporary working copy subprocess.check_call(["rm", "-rf", name]) # class definitions class ElabUser(object): """ Collect the username, group and access control lists for a eLab user """ def __init__(self, name, group): """ initialization of the class """ self.name = name self.group = group self.write_acl = [] self.read_acl = [] def __str__(self): """ return a string representation """ return self.name def __repr__(self): """ return a string representation of the object """ return "" % (self.name, self.group) class AuthzConfigParser(ConfigParser.ConfigParser, object): """ custom functions for parsing the "authz" file as used at cpi there is a dict of users defined, the journals themselves can be accessed via the sections functionality of the ConfigParser base class """ def __init__(self): """ initialization of the class """ self.elab_users = {} super(AuthzConfigParser, self).__init__() def optionxform(self, value): """ reset the method to use cases sensitive names """ return str(value) def read(self, path): """ set up the acl defaults after reading the file """ super(AuthzConfigParser, self).read(path) self.extract_user_info_from_config() def write_to_file(self): with open(AUTHZ_PATH, "w") as filehandle: self.write(filehandle) def write(self, fp): """Write an .ini-format representation of the configuration state. this is adapted from the original library file. changes: - no default section - group-section at top - rest of section sorted by name """ sorted_keys = sorted(self._sections.keys()) sorting = ["groups"] sorting.extend([k for k in sorted_keys if k <> "groups"]) for section in sorting: fp.write("[%s]\n" % section) acls = dict( (k, v) for k, v in self._sections[section].items() if k != "__name__") if section != "groups": for group in (ADMINS, USERS, RESTRICTED, ALUMNI): group_id = "@" + group acl_value = acls.pop(group_id, GROUP_DEFAULTS[group]) key = " = ".join((group_id, str(acl_value).replace('\n', '\n\t'))) fp.write("%s\n" % (key)) for (key, value) in acls.items(): if (value is not None) or (self._optcre == self.OPTCRE): key = " = ".join((key, str(value).replace('\n', '\n\t'))) fp.write("%s\n" % (key)) fp.write("\n") def extract_user_info_from_config(self): """ extracts the user information from the config file the information of the journals can be accessed via get_journal_info """ # first parse the group definitions for group, userlist in self.items("groups"): if group not in GROUP_DEFAULTS: raise Exception("Undefined group " + group) for username in re_separators.split(userlist): if username in self.elab_users: raise Exception("Found duplicate entry for user " + username) self.elab_users[username] = ElabUser(username, group) # walk through the sections to get individual acl information for section in self.sections(): if not section.endswith(SVN_SUFFIX): # skip all entries in the config, that are not lab journals continue for (option, value) in self.items(section): if option in self.elab_users: # a nicer name for the lab journal belongs_to = section[:-2] # a acl entry for a user if value.lower() == WRITE_ACL: self.elab_users[option].write_acl.append(belongs_to) elif value.lower() == READ_ACL: self.elab_users[option].read_acl.append(belongs_to) def group_users(self): """ uses the list of users to group them by their group name """ groups = dict() for user in self.elab_users.values(): if user.group not in groups: groups[user.group] = [] groups[user.group].append(user.name) return groups def add_journal_acl_for(self, username, group): """ sets the acls for a new user an the corresponding journal """ self.elab_users[username] = ElabUser(username, group) journal_path = username + SVN_SUFFIX self.add_section(journal_path) self.set(journal_path, username, WRITE_ACL) for group, acl in GROUP_DEFAULTS.items(): self.set(journal_path, "@"+group, acl) self._update_user_group_config() def move_user_to_alumni(self, name): """ moves a user to the alumni group and removes the acl privileges """ user = self.elab_users[name] user.group = ALUMNI for access_to in user.write_acl: self.remove_option(access_to + SVN_SUFFIX, user.name) for access_to in user.read_acl: self.remove_option(access_to + SVN_SUFFIX, user.name) self._update_user_group_config() def _update_user_group_config(self): """ updates the config settings of the groups section """ groups = self.group_users() for group, userlist in groups.items(): self.set("groups", group, ", ".join(sorted(userlist))) def get_journal_info(self, name): """ returns read and write access info of an lab journal """ if not name.endswith(SVN_SUFFIX): name = name + SVN_SUFFIX if not self.has_section(name): return None info = { WRITE_ACL: [], READ_ACL: [] } for (option, value) in self.items(name): if value in (WRITE_ACL, READ_ACL): info[value].append(option) return info if __name__ == "__main__": # create configparser instance config = AuthzConfigParser() # read config file config.read(AUTHZ_PATH) # command line interface: # no option: display info # -g display users in a group # -a add regular user # -r add restricted user # -m move to alumni # -p reset user password parser = optparse.OptionParser( usage="usage: %prog [option] name", description="shows and manipulates svn access rights", epilog="to grant a restricted user access to another folder, you have to carefully edit the authz file") parser.add_option("-g", "--groupinfo", action="store_const", dest="what", const="g", help="display users in a group") parser.add_option("-a", "--add", action="store_const", dest="what", const="a", help="add a regular user") parser.add_option("-r", "--restricted", action="store_const", dest="what", const="r", help="add a restricted user") parser.add_option("-m", "--move", action="store_const", dest="what", const="m", help="move a user to alumni") parser.add_option("-p", "--password", action="store_const", dest="what", const="p", help="reset a user password") options, args = parser.parse_args() if len(args)==0: # no arguments? then display all the users! groups = config.group_users() for name, usernames in groups.items(): print "Users in group '%s':" % name for name in sorted(usernames): print " " + name sys.exit() if len(args)>1: # more than one usename? not here, john boy sys.exit("please provide only one name") name = args[0] if options.what == "g": # show group information groups = config.group_users() if name not in groups: sys.exit("Group not found") print "Users in group '%s':" % name for usernamename in sorted(groups[name]): print " " + usernamename sys.exit() if options.what in ("a", "r"): # add a user, restricted or regular if name in config.elab_users: sys.exit("Username '%s' already in use" % name) group = RESTRICTED if options.what == "r" else USERS config.add_journal_acl_for(name, group) create_new_repository(name) #subprocess.check_call(SVN_DIR_CREATOR + " " + name, shell=True) password = set_new_password(name) print "New password for :" print "username: " + name print "password: " + password print "url: https://svn.cpi.imtek.uni-freiburg.de/" + name config.write_to_file() sys.exit() # from here downwards we need already existent usernames if name not in config.elab_users: sys.exit("User '%s' not found, use this without a name to get a list of users." % name) if options.what == "m": # move user to alumni user = config.elab_users[name] if user.group == ALUMNI: sys.exit("User '%s' is already in group '%s'" % (name, ALUMNI)) if user.group == ADMINS: sys.exit("User '%s' is in group '%s', will not moved to '%s'" % (name, ADMINS, ALUMNI)) config.move_user_to_alumni(name) config.write_to_file() delete_password(name) sys.exit() if options.what == "p": # reset a password password = set_new_password(name) print "New password for :" print "username: " + name print "password: " + password sys.exit() # no option, just a name: user = config.elab_users[name] print "User %s is in group '%s':" % (name, user.group) # print the write acls for a user if user.group == ADMINS: print " Write access is granted to all journals." elif user.write_acl: write_acl = [ username + SVN_SUFFIX for username in user.write_acl ] print " Write access is granted to '%s'. " % "', '".join(write_acl) else: print " Write access is NOT granted to any journals" # print the read acls for a user if user.group == ADMINS: print " Read access is granted to all journals." elif user.group == USERS: print " Read access is granted to (nearly) all journals." elif user.read_acl: read_acl = [ username + SVN_SUFFIX for username in user.read_acl ] print " Read access is granted to '%s'. " % "', '".join(read_acl) else: print " Read access is NOT granted to any journals" info = config.get_journal_info(name) # print the write acls for a journal print "Labjournal %s%s" % (name, SVN_SUFFIX) if info[WRITE_ACL]: print " Write access granted to: " + ", ".join(info[WRITE_ACL]) else: print " No write access granted to anybody" # print the read acls for a journal if info[READ_ACL]: print " Read access granted to: " + ", ".join(info[READ_ACL]) else: print " No read access granted to anybody"