From 5987d349aa9feebcea2347944af6e5dcbb8a9188 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Fri, 7 Feb 2014 14:41:43 +0100 Subject: [PATCH] initial commit, first working copy --- README | 104 ++++++++++++ authz | 428 ++++++++++++++++++++++++++++++++++++++++++++++++ htpasswd | 6 + manage.py | 288 ++++++++++++++++++++++++++++++++ svn-dir-creator | 28 ++++ 5 files changed, 854 insertions(+) create mode 100644 README create mode 100644 authz create mode 100644 htpasswd create mode 100644 manage.py create mode 100644 svn-dir-creator diff --git a/README b/README new file mode 100644 index 0000000..b0fa479 --- /dev/null +++ b/README @@ -0,0 +1,104 @@ +SVN USER MANAGEMENT README +========================== + +First of all, we talk about the following files and folders: + + .htpasswd + .svn-dir-creator + README + authz + cpi + elab-users.py + old-scripts-backup + +** WARNING: ** +In this list, there are two hidden files: `.htpasswd` and +`.svn-dir-creator`. This two are hidden on purpose, so +** don't mess with these files **. + + +quick file overview +------------------- + +`.htpasswd`: stores the passwords for the users (classical apache htpaswd-file) + +`.svn-dir-creator`: creates empty svn directories for new users + +`README`: this file + +`authz`: defines the access controll list, so who has access to what + +`cpi`: folder that holds the svn repository itself + +`elab-users.py`: usermanagement script + +`old-scripts-backup`: contains the old scripts to add a user for backup reasons + + +Usermanagement with `elab-users.py` +----------------------------------- + +** HINT: ** To run this scipt first change to the directory with `cd /var/www/svn` and prepend every command with `./` (e.g. `./elab-users.py --help`). + +The script `elab-users.py` provides some options to add and delete users, +show access information from users and their elab journals. If the scprit +is called with the `--help` option, the folowing help message is displayed: + + Usage: elab-users.py [option] name + + shows and manipulates svn access rights + + Options: + -h, --help show this help message and exit + -g, --groupinfo display users in a group + -a, --add add a regular user + -r, --restricted add a restricted user + -m, --move move a user to alumni + -p, --password reset a user password + + to grant a restricted user access to another folder, you have to carefully + edit the authz file + +the following combinations are possible: + +* `elab-users.py`: will show a list of all groups and their users +* `elab-users.py UserName`: shows the access rights of the user and their labjournal +* `elab-users.py -g GroupName`: shows a list of all group members +* `elab-users.py -a UserName`: adds a regular user, creates svn folders and sets a random password +* `elab-users.py -a UserName`: adds a restricted user, creates svn folders and sets a random password +* `elab-users.py -m UserName`: moves an existing user to the alumni group, removes his password +* `elab-users.py -p UserName`: resets the password for an existing user to a new random one + + +Grant read writes to restricted users +------------------------------------- + +As noted in the help message of `elab-users.py`, if a restriced user should have +read access to another labjournal, the `authz` file has to be edited manually. +Here are two examples that grant the user 'UrmilShah' read access to +two different lab journals: + + ... snip ... + + [cpi:/AndreasEvers] + @restricted = + UrmilShah = r + + ... snip ... + + [cpi:/HolgerFrey] + @restricted = + UrmilShah = r + HolgerFrey = rw + + ... snip ... + +This does not apply to regular users, since these have read access to all folders. + + +Hint +---- + +This readme is written in Markdown. +So if you want a nice printout, use a markdown converter first. +Something like diff --git a/authz b/authz new file mode 100644 index 0000000..cc6a751 --- /dev/null +++ b/authz @@ -0,0 +1,428 @@ +[groups] +administrators = OswaldPrucker +restricted = AndreasEver, ArthurMartens, BeniPrasser, JuliaSaar, SimonZunker, SirasaYodmongkol, UrmilShah, YongZhou +alumni = AlexeyKopyshev, AndreasBoenisch, AnkeWoerz, AnneLoesche, ArulGeetha, ChristianSchuh, ChristineBunte, CkPandiyarajan, FanWu, GinoRodriguez, GuillermoBenites, HeikeHaller, IrenaEipert, JacobBelardi, JenniferPfau, JoachimLauterwasser, JohannesBaader, KatrinMoosmann, KerstinSchuh, KimberlySimancas, MarcelHoffmann, MarcoArmbruster, MariaVoehringer, MariaVohringer, MartinaAuerswald, MartinVellinger, MatthiasLischka, MessRechner, MichaelaFrase, MiriamScheckenbach, MonicaPerez, MonikaKurowska, NinoLomadze, Nongluck, OliverDornfeld, PeterZahn, PhilippDiefenthaler, PhilippWollermann, RodrigoNavarro, SaraFuchs, SebastianBoehmer, SebastianSebald, SimonBodendorfer, SimonSchuster, ThidaratWangkam, TobiasKoenig, TristanBourrel, UlrikeRiehle, ViVek, WolfgangEhm, YnSekhar, ZouStaarter +users = AlexanderDietz, AliciaMalekLuz, AndreasMader, AnnaSchuler, AnneBuderer, CanerKaganaslan, ChristophScheibelein, DanielaMoessner, DavidBoschert, DavidSchwaerzle, FrankScherag, FranziskaDorner, GerhardBaaken, GregorOsterwinter, HolgerFrey, JanNiklasSchoenberg, JonGreen, KarenLienkamp, KeLi, MalwinaPajestka, MaraFlorea, MarcelRothfelder, MarcZinggeler, MarcelHoffmann, MartinKoerner, MartinRendl, MartinSchoenstein, MatthiasMenzel, MelanieEichhorn, MichaelHenze, MostafaMahmoud, NataliaSchatz, NicoleBirsner, NilsKorf, PetraHettich, PhilipKotrade, RaduCristianMutihac, RebeccaBlell, RomanErath, SamarKazan, SaschaEngel, ShararehSahneh, SureshReddyBanda, ThomasBrandstetter, TianyangZheng, TobiasHeitzler, VinicioCarias, VitaliyKondrashov, WibkeHartleb, XiaoqiangHou + +[cpi:/] +@admins = rw +@users = r +@restricted = r +@alumni = + +[cpi:/AlexanderDietz] +@restricted = +AlexanderDietz = rw + +[cpi:/AlexeyKopyshev] +@restricted = + +[cpi:/AliciaMalekLuz] +@restricted = +AliciaMalekLuz = rw + +[cpi:/AndreasBoenisch] +@restricted = + +[cpi:/AndreasEver] +@restricted = +AndreasEver = rw + +[cpi:/AndreasEvers] +@restricted = +UrmilShah = r + +[cpi:/AndreasMader] +@restricted = +AndreasMader = rw +SirasaYodmongkol = r + +[cpi:/AnkeWoerz] +@restricted = + +[cpi:/AnnaSchuler] +@restricted = +AnnaSchuler = rw + +[cpi:/AnneBuderer] +@restricted = +AnneBuderer = rw + +[cpi:/AnneLoesche] +@restricted = + +[cpi:/AnselmHoppmann] +@restricted = + +[cpi:/ArthurMartens] +@restricted = +ArthurMartens = rw + +[cpi:/ArulGeetha] +@restricted = + +[cpi:/BeniPrasser] +@restricted = +BeniPrasser = rw + +[cpi:/CanerKaganaslan] +@restricted = +CanerKaganaslan = rw + +[cpi:/ChristianSchuh] +@restricted = + +[cpi:/ChristineBunte] +@restricted = + +[cpi:/ChristophScheibelein] +@restricted = +ChristophScheibelein = rw + +[cpi:/CkPandiyarajan] +@restricted = + +[cpi:/DanielaMoessner] +@restricted = +DanielaMoessner = rw + +[cpi:/DavidBoschert] +@restricted = +DavidBoschert = rw + +[cpi:/DavidSchwaerzle] +@restricted = +DavidSchwaerzle = rw + +[cpi:/DennisTrenkle] +@restricted = + +[cpi:/DingdingHe] +@restricted = + +[cpi:/FanWu] +@restricted = + +[cpi:/FrankScherag] +@restricted = +FrankScherag = rw + +[cpi:/FranziskaDorner] +@restricted = +FranziskaDorner = rw + +[cpi:/GerhardBaaken] +@restricted = +GerhardBaaken = rw + +[cpi:/GinoRodriguez] +@restricted = + +[cpi:/GregorOsterwinter] +@restricted = +GregorOsterwinter = rw + +[cpi:/GuillermoBenites] +@restricted = + +[cpi:/HeikeHaller] +@restricted = + +[cpi:/HolgerFrey] +@restricted = +HolgerFrey = rw +UrmilShah = r + +[cpi:/IrenaEipert] +@restricted = + +[cpi:/JacobBelardi] +@restricted = + +[cpi:/JanNiklasSchoenberg] +@restricted = +JanNiklasSchoenberg = rw + +[cpi:/JenniferPfau] +@restricted = + +[cpi:/JoachimLauterwasser] +@restricted = + +[cpi:/JohannesBaader] +@restricted = + +[cpi:/JonGreen] +@restricted = +JonGreen = rw + +[cpi:/JonasGroten] +@restricted = + +[cpi:/JuliaSaar] +@restricted = +JuliaSaar = rw + +[cpi:/KarenLienkamp] +@restricted = +KarenLienkamp = rw + +[cpi:/KatrinMoosmann] +@restricted = + +[cpi:/KeLi] +@restricted = + +[cpi:/KerstinSchuh] +@restricted = + +[cpi:/KimberlySimancas] +@restricted = + +[cpi:/MalwinaPajestka] +@restricted = +MalwinaPajestka = rw + +[cpi:/MaraFlorea] +@restricted = +MaraFlorea = rw + +[cpi:/MarcelRothfelder] +@restricted = +MarcelRothfelder = rw + +[cpi:/MarcZinggeler] +@restricted = +MarcZinggeler = rw + +[cpi:/MarcelHoffmann] +@restricted = +MarcelHoffmann = rw + +[cpi:/MarcoArmbruster] +@restricted = + +[cpi:/MariaVoehringer] +@restricted = + +[cpi:/MartinaAuerswald] +@restricted = + +[cpi:/MartinKoerner] +@restricted = +MartinKoerner = rw + +[cpi:/MartinMarazita] +@restricted = + +[cpi:/MartinRendl] +@restricted = +MartinRendl = rw +ArthurMartens = r + +[cpi:/MartinSchoenstein] +@restricted = +MartinSchoenstein = rw + +[cpi:/MartinVellinger] +@restricted = + +[cpi:/MatthiasLischka] +@restricted = + +[cpi:/MatthiasMenzel] +@restricted = +MatthiasMenzel = rw + +[cpi:/MaxMustermann] +@restricted = + +[cpi:/MelanieEichhorn] +@restricted = +MelanieEichhorn = rw + +[cpi:/MessRechner] +@restricted = + +[cpi:/MichaelHenze] +@restricted = +MichaelHenze = rw + +[cpi:/MichaelaFrase] +@restricted = + +[cpi:/MiriamScheckenbach] +@restricted = + +[cpi:/MonicaPerez] +@restricted = + +[cpi:/MonikaKurowska] +@restricted = + +[cpi:/MostafaMahmoud] +@restricted = +MostafaMahmoud = rw + +[cpi:/NataliaSchatz] +@restricted = +NataliaSchatz = rw + +[cpi:/NicolasSchorr] +@restricted = + +[cpi:/NicoleBirsner] +@restricted = +NicoleBirsner = rw + +[cpi:/NilsKorf] +@restricted = +NilsKorf = rw + +[cpi:/NinoLomadze] +@restricted = + +[cpi:/Nongluck] +@restricted = + +[cpi:/OliverDornfeld] +@restricted = + +[cpi:/OswaldPrucker] +@restricted = + +[cpi:/PengZou] +@restricted = + +[cpi:/PeterZahn] +@restricted = + +[cpi:/PetraHettich] +@restricted = +PetraHettich = rw + +[cpi:/PhilippDiefenthaler] +@restricted = + +[cpi:/PhilipKotrade] +@restricted = +PhilipKotrade = rw + +[cpi:/RaduCristianMutihac] +@restricted = +RaduCristianMutihac = rw + +[cpi:/RebeccaBlell] +@restricted = +RebeccaBlell = rw + +[cpi:/RodrigoNavarro] +@restricted = + +[cpi:/RomanErath] +@restricted = +RomanErath = rw + +[cpi:/SamarKazan] +@restricted = +SamarKazan = rw + +[cpi:/SaraFuchs] +@restricted = + +[cpi:/SaschaEngel] +@restricted = +SaschaEngel = rw + +[cpi:/SebastianBoehmer] +@restricted = + +[cpi:/ShararehSahneh] +@restricted = +ShararehSahneh = rw + +[cpi:/SimonBodendorfer] +@restricted = + +[cpi:/SimonEbner] +@restricted = + +[cpi:/SimonSchuster] +@restricted = + +[cpi:/SimonZunker] +@restricted = +SimonZunker = rw + +[cpi:/SirasaYodmongkol] +@restricted = +SirasaYodmongkol = rw + +[cpi:/SureshReddyBanda] +@restricted = +SureshReddyBanda = rw + +[cpi:/ThidaratWangkam] +@restricted = + +[cpi:/ThomasBrandstetter] +@restricted = +ThomasBrandstetter = rw + +[cpi:/TianyangZheng] +@restricted = +TianyangZheng = rw + +[cpi:/TobiasHeitzler] +@restricted = +TobiasHeitzler = rw + +[cpi:/TobiasKoenig] +@restricted = + +[cpi:/TristanBourrel] +@restricted = + +[cpi:/UlrikeRiehle] +@restricted = + +[cpi:/UrmilShah] +@restricted = +UrmilShah = rw + +[cpi:/ViVek] +@restricted = + +[cpi:/VinicioCarias] +@restricted = +VinicioCarias = rw + +[cpi:/VitaliyKondrashov] +@restricted = +VitaliyKondrashov = rw +SimonZunker = r + +[cpi:/WibkeHartleb] +@restricted = +WibkeHartleb = rw + +[cpi:/WolfgangEhm] +@restricted = + +[cpi:/XiaoqiangHou] +@restricted = +XiaoqiangHou = rw + +[cpi:/YnSekhar] +@restricted = + +[cpi:/YongZhou] +@restricted = +YongZhou = rw + +[cpi:/ZhuolingDeng] +@restricted = + +[cpi:/ZouStaarter] +@restricted = + diff --git a/htpasswd b/htpasswd new file mode 100644 index 0000000..af66e5c --- /dev/null +++ b/htpasswd @@ -0,0 +1,6 @@ +foo:$apr1$SzJRyvJU$U3luHwCA6xHfKowizE.Gl. +FOO:$apr1$LSPDdLqg$tiGbDGgNEXcRA/oyadYSw1 +AndreasEvers:$apr1$n0Oaok6e$wyHcUg6Upm9sE2AoYlVMO/ +FOOBar:$apr1$pZCbClF5$smEDwhMJIVmPsNmMEkRPd1 +FooBar:$apr1$24r9zF2e$9q30fNOqSlvn6itdhZMpc1 +UrmilShh:$apr1$WxMGE8Wb$H0xWao6KZGqBJoXj7fJ420 diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..0a21b0e --- /dev/null +++ b/manage.py @@ -0,0 +1,288 @@ +#/usr/bin/python + +# imports of modules +import ConfigParser +import optparse +import os +import re +import random +import string +import subprocess +import sys + +# defining some constants +AUTHZ_PATH = "authz" +HTPWD_PATH = "htpasswd" +SVN_DIR_CREATOR = "svn-dir-creator" +SVN_BASE = "cpi:/" + +ADMINS = "administrators" +REGULAR = "users" +RESTRICTED = "restricted" +ALUMNI = "alumni" + +READ_ACL = "r" +WRITE_ACL = "rw" + +re_separators = re.compile("[\t ,;]+") + +# helper functions +def group_users(users): + """ uses the list of users to group them by their group name """ + groups = dict() + for user in users.values(): + if user.group not in groups: + groups[user.group] = [] + groups[user.group].append(user.name) + return groups + +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, length=10): + """ 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) + + + +# class definitions +class User(object): + """ Collect the username, group and access control lists """ + + 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 """ + + def __init__(self): + """ initialization of the class """ + self.users = None + super(AuthzConfigParser, self).__init__() + + def optionxform(self, value): + """ reset the method to use cases ensitive names """ + return str(value) + + def extract_users(self): + """ extract user information from config """ + users = dict() + # first we go through the groups, as found in the groups section of the + # authz file + for group, userlist in self.items("groups"): + for username in re_separators.split(userlist): + if username in users: + raise Exception("Found duplicate entry for user " + username) + user = User(username, group) + users[username] = user + # second we scan each section that is related to an svn folder (it + # starts with the svn base) for read and write access user entries + for section in self.sections(): + if section.startswith(SVN_BASE): + belongs_to = section.lstrip(SVN_BASE) + for (option, value) in self.items(section): + if option in users: + if value.lower() == WRITE_ACL: + users[option].write_acl.append(belongs_to) + elif value.lower() == READ_ACL: + users[option].read_acl.append(belongs_to) + # return the userlist + return users + + def get_folder_info(self, name): + """ returns read and write access info of an svn folder """ + if not name.startswith(SVN_BASE): + name = SVN_BASE + name + 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 + + def move_user_to_alumni(self, user): + """ moves a user to the alumni group and removes every access rights """ + for access_to in user.write_acl: + folder = SVN_BASE + access_to + self.remove_option(folder, user.name) + for access_to in user.read_acl: + folder = SVN_BASE + access_to + self.remove_option(folder, user.name) + user.write_acl = [] + user.read_acl = [] + user.group = ALUMNI + delete_password(user.name) + + def update_user_groups(self, users): + """ updates the config settings of the groups section """ + groups = group_users(users) + for group, userlist in groups.items(): + self.set("groups", group, ", ".join(sorted(userlist))) + + 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) + for (key, value) in self._sections[section].items(): + if key == "__name__": + continue + 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") + + +if __name__ == "__main__": + # create configparser instance + config = AuthzConfigParser() + # change option name transformation to case sensitive + config.optionxform = str + # read config file + config.read(AUTHZ_PATH) + users = config.extract_users() + + # 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 = group_users(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 = group_users(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 users: + sys.exit("Username '%s' already in use" % name) + group = RESTRICTED if options.what == "r" else REGULAR + users[name] = User(name, group) + config.update_user_groups(users) + folder = SVN_BASE + name + config.add_section(folder) + config.set(folder, "@"+RESTRICTED, "") + config.set(folder, name, WRITE_ACL) + #subprocess.check_call(SVN_DIR_CREATOR + " " + name, shell=True) + password = set_new_password(name) + print "New password for user '%s': '%s'" % (name, password) + config.write_to_file() + sys.exit() + + # from here downwards we need already existent usernames + if name not in users: + sys.exit("User '%s' not found, use this without a name to get a list of users." % name) + user = users[name] + + if options.what == "m": + # move user to alumni + groups = group_users(users) + 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(user) + config.update_user_groups(users) + config.write_to_file() + 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: + # print all the infos connected to a name + print "User %s is in group '%s':" % (name, user.group) + if user.group == ADMINS: + print " Write access is granted to all folders." + elif user.write_acl: + write_acl = [ SVN_BASE + username for username in user.write_acl ] + print " Write access is granted to folders '%s'. " % "', '".join(write_acl) + else: + print " Write access is NOT granted to any folder" + if user.group in (ADMINS, REGULAR): + print " Read access is granted to all folders." + elif user.read_acl: + read_acl = [ SVN_BASE + username for username in user.read_acl ] + print " Read access is granted to folders '%s'. " % "', '".join(read_acl) + else: + print " Read access is NOT granted to any folder" + info = config.get_folder_info(name) + print "Labjornal %s%s:" % (SVN_BASE, name) + write_acl = [ "@" + ADMINS ] + info[WRITE_ACL] + print " Write access granted to " + ", ".join(write_acl) + read_acl = [ "@" + ADMINS, "@" + REGULAR ] + info[READ_ACL] + print " Read access granted to: " + ", ".join(read_acl) diff --git a/svn-dir-creator b/svn-dir-creator new file mode 100644 index 0000000..c5a9074 --- /dev/null +++ b/svn-dir-creator @@ -0,0 +1,28 @@ +#!/bin/bash + +# This script will add a new user directory to the svn repository +# it will only checkout the top level directory and should therefore +# be faster then the original script +# HF + +set -e +set -u + +username=$1 +year=`date +"%Y"` + +# checkout only the top level directory into a new folder +cd /var/www +svn checkout file:///var/www/svn/cpi svn-dirs-empty --depth immediates + +# create the user folder and commit it +cd /var/www/svn-dirs-empty +mkdir -p $username +mkdir -p $username/$year/$year-{01,02,03,04,05,06,07,08,09,10,11,12} +for i in $username/$year/*; do touch $i/.empty; done +svn add $username +svn commit -m"New user: $username" + +# remove the temporary directory +cd /var/www +rm -r svn-dirs-empty/