|
|
|
#!/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", "sshfs-for-svn")
|
|
|
|
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 "<User '%s@%s'>" % (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" % username)
|
|
|
|
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 user '%s': '%s'" % (name, password)
|
|
|
|
print "http://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 user '%s': '%s'" % (name, 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"
|