From 82aaff86a5aefbadc0e520949976ec11e312d2b6 Mon Sep 17 00:00:00 2001 From: mcrute Date: Thu, 24 Feb 2011 00:11:42 -0500 Subject: Initial import --- .hgignore | 5 + lock-repo.py | 37 +++++++ repolib.py | 275 ++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts.log | 119 +++++++++++++++++++++++ server.fcgi | 11 +++ sync-repo-config.py | 55 +++++++++++ validate-login.py | 63 ++++++++++++ 7 files changed, 565 insertions(+) create mode 100644 .hgignore create mode 100755 lock-repo.py create mode 100644 repolib.py create mode 100644 scripts.log create mode 100755 server.fcgi create mode 100755 sync-repo-config.py create mode 100755 validate-login.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..88f8e1d --- /dev/null +++ b/.hgignore @@ -0,0 +1,5 @@ +^\.ssh/ +^repos/ +\.pyc$ +^\.viminfo$ +^\.bash_history$ diff --git a/lock-repo.py b/lock-repo.py new file mode 100755 index 0000000..f651d3a --- /dev/null +++ b/lock-repo.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# vim: set filencoding=utf8 +""" +Mercurial Shared SSH Repo Lock Script + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Hosting +@date: February 23, 2011 +""" + +import os +import repolib + +def main(argv): + log = repolib.get_logger('validate-login') + + if ('SSH_HG_REPO' not in os.environ or + 'SSH_HG_USER' not in os.environ): + log.error("Failed to execute pre-lock checks") + return 1 + + try: + repo = repolib.Repository(os.environ['SSH_HG_REPO']) + repo.load_from_hgrc() + except IOError: + log.error("Could not load repository config") + return 1 + + if not repo.can_be_written_by(os.environ['SSH_HG_USER']): + log.error("You can not write to this repository") + return 1 + + return 0 + +if __name__ == "__main__": + import sys + sys.exit(main(sys.argv[1:])) diff --git a/repolib.py b/repolib.py new file mode 100644 index 0000000..1ba4814 --- /dev/null +++ b/repolib.py @@ -0,0 +1,275 @@ +# vim: set filencoding=utf8 +""" +Mercurial Repository Management Library + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Hosting +@date: February 23, 2011 +""" + +import os.path +import logging +from StringIO import StringIO +from ConfigParser import SafeConfigParser + + +class Adornments(object): + + BASE_CSS = ("padding: 0px 4px;" "font-size: 10px;" + "font-weight: normal;" "border: 1px solid;" + "background-color: #fafafa;" + "border-color: #ffffff #f0f0f0 #f0f0f0 #ffffff;") + + CSS_YELLOW = ("background-color: #ffe500;" "color: #222222;" + "border-color: #fff066 #a89e43 #a89e43 #fff066;") + + CSS_BROWN = ("background-color: #815555;" "color: #ffffff;" + "border-color: #ff3347 #541118 #541118 #ff3347;") + + CSS_GREEN = ("background-color: #11a800;" "color: #ffffff;" + "border-color: #5eff4c #3ea832 #3ea832 #5eff4c;") + + CSS_BLUE = ("background-color: #0099ff;" "color: #ffffff;" + "border-color: #4cb8ff #3279a8 #3279a8 #4cb8ff;") + + CSS_RED = ("background-color: #ff0000;" "color: #ffffff;" + "border-color: #ff6666 #000000 #000000 #ff6666;") + + BASE_HTML = '{{tag}} '.format(BASE_CSS) + LINK_CSS_DARK = 'color: #ffffff; text-decoration: none;' + LINK_CSS_LIGHT = 'color: #000000; text-decoration: none;' + + def __init__(self, repo): + self.repo = repo + + def __str__(self): + if self.repo.moved_to: + tag = 'MOVED' + tag = tag.format(link_css=self.LINK_CSS_DARK, + url=self.repo.moved_to) + return self.BASE_HTML.format(css=self.CSS_BLUE, tag=tag) + + if self.repo.defunct: + return self.BASE_HTML.format(css=self.CSS_BROWN, tag="DEFUNCT") + + if self.repo.upstream: + tag = 'FORK' + tag = tag.format(link_css=self.LINK_CSS_LIGHT, + url=self.repo.upstream) + return self.BASE_HTML.format(css=self.CSS_YELLOW, tag=tag) + + return "" + + +class Repository(object): + + def __init__(self, path, description=None, contact=None): + self.path = path + self.description = description + self.contact = contact + self.readers = [] + self.writers = [] + self.defunct = False + self.moved_to = None + self.repo_path = None + self.upstream = None + self.lock_script = None + + def can_be_read_by(self, user): + return user in self.readers + + def can_be_written_by(self, user): + return user in self.writers + + @property + def exists(self): + return os.path.exists(self.full_path) + + @property + def full_path(self): + return os.path.join(self.repo_path, self.path) + + @property + def hgrc_path(self): + return os.path.join(self.full_path, '.hg', 'hgrc') + + @classmethod + def from_config(cls, config, repo_path, lock_script=None): + self = cls(config['path'], config.get('description', None), + config.get('contact', None)) + + self.readers = ConfigLoader.as_list(config.get('read', '')) + self.writers = ConfigLoader.as_list(config.get('write', '')) + + self.defunct = ConfigLoader.as_bool(config.get('defunct', 'no')) + self.moved_to = config.get('moved_to', None) + self.upstream = config.get('upstream', None) + self.upstream = config.get('upstream', None) + + self.repo_path = repo_path + self.lock_script = lock_script + + return self + + def build_hgrc(self, users): + buf = StringIO() + + buf.write("# This file is generated by sync-repo-config\n") + buf.write("# Changes are over-written on each run\n\n") + + if self.contact or self.description or self.writers: + buf.write("[web]\n") + + if self.contact: + buf.write("contact = {0}\n".format(users[self.contact])) + + if self.description: + buf.write("description = {adornments}" + " {self.description}\n".format( + adornments=Adornments(self), self=self)) + + if self.writers: + buf.write("allow_push = {0}\n".format(",".join(self.writers))) + + buf.write("\n") + + if self.lock_script: + buf.write("[hooks]\n") + buf.write("pretxnchangegroup.deny.lock = {0}\n".format( + self.lock_script)) + buf.write("\n") + + buf.write("[ssh]\n") + buf.write("readers = {0}\n".format(",".join(self.readers))) + buf.write("writers = {0}\n".format(",".join(self.writers))) + + return buf.getvalue() + + def write_hgrc(self, users): + if not self.exists: + raise IOError("Repository {0!r} does not exist".format( + self.full_path)) + + with open(self.hgrc_path, 'w') as hgrc: + hgrc.write(self.build_hgrc(users)) + + def load_hgrc(self): + hgrc = SafeConfigParser() + with open(self.hgrc_path, 'r') as fp: + hgrc.readfp(fp) + + return hgrc + + def load_from_hgrc(self): + hgrc = self.load_hgrc() + + self.description = hgrc.get("web", "description") + self.contact = hgrc.get("web", "contact") + self.readers = ConfigLoader.as_list(hgrc.get("ssh", "readers")) + self.writers = ConfigLoader.as_list(hgrc.get("ssh", "writers")) + + +class User(object): + + def __init__(self, username, name, email): + self.username = username + self.name = name + self.email = email + self.can_create = False + self.ssh_key = None + self.login_script = None + + def __str__(self): + return "{self.name} <{self.email}>".format(self=self) + + @classmethod + def from_config(cls, config, login_script): + self = cls(config['username'], config['name'], config['email']) + + self.can_create = ConfigLoader.as_bool(config['can_create']) + self.ssh_key = config['ssh_key'] + self.login_script = login_script + + return self + + @property + def ssh_line(self): + return ('command="{self.login_script} {self.username}",' + 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding' + ' {self.ssh_key} {self.email}\n').format(self=self) + + +class ConfigLoader(object): + + def __init__(self, cfg_file): + self.config = SafeConfigParser() + with open(cfg_file, 'r') as config_file: + self.config.readfp(config_file) + + self.login_script = self.config.get('system', 'login_script') + self.repo_path = self.config.get('system', 'repo_path') + self.repo_user = self.config.get('system', 'repo_user') + self.lock_script = self.config.get('system', 'lock_script') + + def _filtered_sections(self, section_name): + for section in self.config.sections(): + if section.startswith(section_name + ":"): + yield section + + @staticmethod + def as_bool(value): + return value.lower() in ('yes', 'true', 'on') + + @staticmethod + def as_list(value): + return [item.strip() for item in value.split(",") + if value] + + @staticmethod + def clean_section_name(name): + return name.split(':')[1] + + @property + def repos(self): + for section in self._filtered_sections('repo'): + values = dict(self.config.items(section)) + values['path'] = self.clean_section_name(section) + + yield Repository.from_config(values, self.repo_path, + self.lock_script) + + @property + def users(self): + for section in self._filtered_sections('user'): + values = dict(self.config.items(section)) + values['username'] = self.clean_section_name(section) + + yield User.from_config(values, self.login_script) + + @property + def user_dict(self): + return dict((user.username, user) for user in self.users) + + @property + def repo_user_authorized_keys(self): + home_dir = os.path.expanduser('~' + self.repo_user) + + if '~' in home_dir: + raise ValueError("User {0!r} doesn't exist".format(self.repo_user)) + + return os.path.join(home_dir, '.ssh', 'authorized_keys') + + +def get_logger(name): + format = "%(levelname)s: %(message)s" + logging.basicConfig(level=logging.DEBUG, format=format, + filename="scripts.log") + + stream = logging.StreamHandler() + stream.setLevel(logging.WARNING) + stream.setFormatter(logging.Formatter(format)) + + logger = logging.getLogger(name) + logger.addHandler(stream) + + return logger diff --git a/scripts.log b/scripts.log new file mode 100644 index 0000000..4684049 --- /dev/null +++ b/scripts.log @@ -0,0 +1,119 @@ +ERROR: Repo /srv/hg/repos/private/vim-site does not exist +INFO: Writing hgrc for 'clients/cameo_tickets_php' +INFO: Writing hgrc for 'forks/hg' +INFO: Writing hgrc for 'private/slate_machine' +INFO: Writing hgrc for 'clients/maxwellness-rewards' +INFO: Writing hgrc for 'mongo_madness' +INFO: Writing hgrc for 'greenbox' +INFO: Writing hgrc for 'codemash_taste_of_cocoa' +INFO: Writing hgrc for 'build_scripts' +INFO: Writing hgrc for 'private/vim-site' +INFO: Writing hgrc for 'clients/finelli_site' +INFO: Writing hgrc for 'private/mcrute_blog_drafts' +INFO: Writing hgrc for 'clients/cameo_tickets_py' +INFO: Writing hgrc for 'milkman' +INFO: Writing hgrc for 'hudson_sc' +INFO: Writing hgrc for 'code_manager' +INFO: Writing hgrc for 'dev_urandom' +INFO: Writing hgrc for 'designer_site' +INFO: Writing hgrc for 'pyrobots' +INFO: Writing hgrc for 'svn_repolist' +INFO: Writing hgrc for 'code_golf' +INFO: Writing hgrc for 'firewall' +INFO: Writing hgrc for 'screen_hooks' +INFO: Writing hgrc for 'avolites_show_cleanup' +INFO: Writing hgrc for 'clients/chapman_site' +INFO: Writing hgrc for 'python_koans' +INFO: Writing hgrc for 'pydepgraph' +INFO: Writing hgrc for 'ventriloquist' +INFO: Writing hgrc for 'myturl_clients' +INFO: Writing hgrc for 'zine-patches' +INFO: Writing hgrc for 'kronos' +INFO: Writing hgrc for 'css_compressor' +INFO: Writing hgrc for 'clients/affordable_site' +INFO: Writing hgrc for 'mcrute_dotfiles' +INFO: Writing hgrc for 'codemash_iphone' +INFO: Writing hgrc for 'clients/brewbuster' +INFO: Writing hgrc for 'clients/cris_site' +INFO: Writing hgrc for 'cardigan' +INFO: Writing hgrc for 'ventriloquy' +INFO: Writing hgrc for 'conductor' +INFO: Writing hgrc for 'pyapache' +INFO: Writing hgrc for 'private/personal_wiki' +INFO: Writing hgrc for 'clients/cameo_website' +INFO: Writing hgrc for 'calendar_proxy' +INFO: Writing hgrc for 'pompom_configs' +INFO: Writing hgrc for 'opendmx_experiments' +INFO: Writing hgrc for 'wordpress_plugins' +INFO: Writing hgrc for 'nose_machineout' +INFO: Writing hgrc for 'awesomebar' +INFO: Writing hgrc for 'hg_sshsign' +INFO: Writing hgrc for 'code_mining' +INFO: Writing hgrc for 'snakeplan' +INFO: Writing hgrc for 'clepy_no_frameworks' +INFO: Writing hgrc for 'full_armor_site' +INFO: Writing hgrc for 'clients/sweet_dreams' +INFO: Writing hgrc for 'mike_crute_org' +INFO: Writing '/srv/hg/.ssh/authorized_keys' +INFO: Writing user 'Mike Crute ' +INFO: Writing user 'Marc Weber ' +INFO: Writing user 'Max Cantor ' +INFO: Writing hgrc for 'clients/cameo_tickets_php' +INFO: Writing hgrc for 'forks/hg' +INFO: Writing hgrc for 'private/slate_machine' +INFO: Writing hgrc for 'clients/maxwellness-rewards' +INFO: Writing hgrc for 'mongo_madness' +INFO: Writing hgrc for 'greenbox' +INFO: Writing hgrc for 'codemash_taste_of_cocoa' +INFO: Writing hgrc for 'build_scripts' +INFO: Writing hgrc for 'private/vim-site' +INFO: Writing hgrc for 'clients/finelli_site' +INFO: Writing hgrc for 'private/mcrute_blog_drafts' +INFO: Writing hgrc for 'clients/cameo_tickets_py' +INFO: Writing hgrc for 'milkman' +INFO: Writing hgrc for 'hudson_sc' +INFO: Writing hgrc for 'code_manager' +INFO: Writing hgrc for 'dev_urandom' +INFO: Writing hgrc for 'designer_site' +INFO: Writing hgrc for 'pyrobots' +INFO: Writing hgrc for 'svn_repolist' +INFO: Writing hgrc for 'code_golf' +INFO: Writing hgrc for 'firewall' +INFO: Writing hgrc for 'screen_hooks' +INFO: Writing hgrc for 'avolites_show_cleanup' +INFO: Writing hgrc for 'clients/chapman_site' +INFO: Writing hgrc for 'python_koans' +INFO: Writing hgrc for 'pydepgraph' +INFO: Writing hgrc for 'ventriloquist' +INFO: Writing hgrc for 'myturl_clients' +INFO: Writing hgrc for 'zine-patches' +INFO: Writing hgrc for 'kronos' +INFO: Writing hgrc for 'css_compressor' +INFO: Writing hgrc for 'clients/affordable_site' +INFO: Writing hgrc for 'mcrute_dotfiles' +INFO: Writing hgrc for 'codemash_iphone' +INFO: Writing hgrc for 'clients/brewbuster' +INFO: Writing hgrc for 'clients/cris_site' +INFO: Writing hgrc for 'cardigan' +INFO: Writing hgrc for 'ventriloquy' +INFO: Writing hgrc for 'conductor' +INFO: Writing hgrc for 'pyapache' +INFO: Writing hgrc for 'private/personal_wiki' +INFO: Writing hgrc for 'clients/cameo_website' +INFO: Writing hgrc for 'calendar_proxy' +INFO: Writing hgrc for 'pompom_configs' +INFO: Writing hgrc for 'opendmx_experiments' +INFO: Writing hgrc for 'wordpress_plugins' +INFO: Writing hgrc for 'nose_machineout' +INFO: Writing hgrc for 'awesomebar' +INFO: Writing hgrc for 'hg_sshsign' +INFO: Writing hgrc for 'code_mining' +INFO: Writing hgrc for 'snakeplan' +INFO: Writing hgrc for 'clepy_no_frameworks' +INFO: Writing hgrc for 'full_armor_site' +INFO: Writing hgrc for 'clients/sweet_dreams' +INFO: Writing hgrc for 'mike_crute_org' +INFO: Writing '/srv/hg/.ssh/authorized_keys' +INFO: Writing user 'Mike Crute ' +INFO: Writing user 'Marc Weber ' +INFO: Writing user 'Max Cantor ' diff --git a/server.fcgi b/server.fcgi new file mode 100755 index 0000000..d8fc53a --- /dev/null +++ b/server.fcgi @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +from flup.server.fcgi_fork import WSGIServer +from mercurial import demandimport; demandimport.enable() +from mercurial.hgweb.hgwebdir_mod import hgwebdir + + +WSGIServer(hgwebdir('/etc/hgweb.cfg'), + bindAddress='/tmp/hgserver.sock', + maxRequests=10, minSpare=1, maxSpare=3, + maxChildren=3).run() diff --git a/sync-repo-config.py b/sync-repo-config.py new file mode 100755 index 0000000..b706e02 --- /dev/null +++ b/sync-repo-config.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# vim: set filencoding=utf8 +""" +Mercurial Shared SSH Repository Metadat Sync + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Hosting +@date: February 23, 2011 +""" + +import repolib + + +def sync_repository_config(repos, users, log): + for repo in repos: + if repo.exists: + log.info("Writing hgrc for %r", repo.path) + repo.write_hgrc(users) + else: + log.warn("Non-existent repo %r", repo.path) + + +def sync_ssh_config(auth_keys_filename, users, log): + with open(auth_keys_filename, 'w') as auth_keys: + log.info("Writing %r", auth_keys_filename) + + for user in users: + log.info("Writing user '%s'", user) + auth_keys.write(user.ssh_line) + + +def main(argv): + log = repolib.get_logger('sync-repo-config') + + try: + cfg_file = argv[-1] if argv else "/etc/hgssh.cfg" + cfg = repolib.ConfigLoader(cfg_file) + except IOError: + log.error("Config file %r doesn't exist", cfg_file) + return 1 + + sync_repository_config(cfg.repos, cfg.user_dict, log) + + try: + sync_ssh_config(cfg.repo_user_authorized_keys, cfg.users, log) + except ValueError, exc: + log.error("%s", exc) + return 1 + + return 0 + + +if __name__ == "__main__": + import sys + sys.exit(main(sys.argv[1:])) diff --git a/validate-login.py b/validate-login.py new file mode 100755 index 0000000..bd87fff --- /dev/null +++ b/validate-login.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# vim: set filencoding=utf8 +""" +Mercurial Shared SSH Login Validator + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Hosting +@date: February 23, 2011 +""" + +import re +import os +import repolib + +from mercurial import demandimport; demandimport.enable() +from mercurial import dispatch + + +def parse_path(): + cmd = os.environ.get('SSH_ORIGINAL_COMMAND', '') + path = re.match("hg -R (\S+) serve --stdio", cmd) + + if path: + return path.groups()[0] + + return None + + +def main(argv): + log = repolib.get_logger('validate-login') + + path = parse_path() + if path: + repo = repolib.Repository(path) + repo.repo_path = os.getcwd() + else: + log.error("Invalid command") + return 1 + + if not repo.exists: + log.error("Repo %s does not exist", repo.full_path) + return 1 + + try: + repo.load_from_hgrc() + except IOError: + log.error("Could not read repo config") + return 1 + + user = argv[-1] + if not repo.can_be_read_by(user): + log.error("You can not read this repository") + return 1 + + os.environ['SSH_HG_USER'] = user + os.environ['SSH_HG_REPO'] = repo.full_path + + dispatch.dispatch(['-R', path, 'serve', '--stdio']) + + +if __name__ == "__main__": + import sys + sys.exit(main(sys.argv[1:])) -- cgit v1.2.3