# HG changeset patch # User Paul Crowley # Date 1213632740 -3600 # Node ID 18e93dbdaf126154e236cde3954322d8c4b4cb6c # Parent 4059dbe9f26a651a18ffefc65a0158b949e48a3c moved stuff into subdirectories diff -r 4059dbe9f26a -r 18e93dbdaf12 access.py --- a/access.py Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -# Copyright 2008 LShift Ltd -# Copyright 2006 Vadim Gelfer -# -# Authors: -# Paul Crowley -# Vadim Gelfer -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. - -from mercurial.i18n import _ -from mercurial.node import * -from mercurial import util - -import os -import ruleset - -class Checker(object): - '''acl checker.''' - - def __init__(self, ui, repo): - self.ui = ui - self.repo = repo - self.rules = ruleset.Ruleset.readfile(os.environ['HG_ACCESS_RULES_FILE']) - self.rules.set(user = os.environ['REMOTE_USER']) - self.rules.set(repo = os.environ['HG_REPO_PATH']) - - def allow(self, node): - '''return if access allowed, raise exception if not.''' - ctx = self.repo.changectx(node) - branch = ctx.branch() - if not self.rules.allow("write", branch=branch, file=None): - return False - for f in ctx.files(): - if not self.rules.allow("write", branch=branch, file=f): - return False - self.ui.debug(_('%s: allowing changeset %s\n') % (__name__, short(node))) - return True - - def check(self, node): - if not self.allow(node): - raise util.Abort(_('%s: access denied for changeset %s') % - (__name__, short(node))) - - -def hook(ui, repo, hooktype, node=None, source=None, **kwargs): - if hooktype != 'pretxnchangegroup': - raise util.Abort(_('config error - hook type "%s" cannot stop ' - 'incoming changesets') % hooktype) - c = Checker(ui, repo) - start = repo.changelog.rev(bin(node)) - end = repo.changelog.count() - for rev in xrange(start, end): - c.check(repo.changelog.node(rev)) - diff -r 4059dbe9f26a -r 18e93dbdaf12 hg-ssh --- a/hg-ssh Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 LShift Ltd -# Copyright 2005-2007 by Intevation GmbH -# Authors: -# Paul Crowley -# Thomas Arendsen Hein -# with ideas from Mathieu PASQUET -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. - -""" -hg-ssh - limit access to hg repositories reached via ssh. Part of -hg-admin-tools. - -This script is called by hg-ssh-wrapper with no arguments - everything -should be in enviroment variables: - -HG_ACCESS_RULES_FILE identifies the path to the rules file -REMOTE_USER the remote user (which is the key used by ssh) -SSH_ORIGINAL_COMMAND the command the user was trying to run - -It uses SSH_ORIGINAL_COMMAND to determine what the user was trying to -do and to what repository, and then checks each rule in the rule file -in turn for a matching rule which decides what to do, defaulting to -disallowing the action. - -""" - -# enable importing on demand to reduce startup time -from mercurial import demandimport; demandimport.enable() - -from mercurial import dispatch - -import sys, os -import ruleset - -def fail(message): - #logfile.write("Fail: %s\n" % message) - sys.stderr.write(message + "\n") - sys.exit(-1) - -def getpath(path): - if path.endswith("/"): - path = path[:-1] - if not ruleset.goodpath(path): - fail("Disallowing path: %s" % path) - return path - -def get_cmd(rules, cmd): - if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): - repo = getpath(cmd[6:-14]) - if rules.allow("read", repo=repo): - os.environ["HG_REPO_PATH"] = repo - return ['-R', repo, 'serve', '--stdio'] - elif cmd.startswith('hg init '): - repo = getpath(cmd[8:]) - if rules.allow("init", repo=repo): - os.environ["HG_REPO_PATH"] = repo - return ['init', repo] - fail("Illegal command %r" % cmd) - -#logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w") -#logfile.write("Started: %s\n" % sys.argv) - -if len(sys.argv) != 1: - fail("hg-ssh must have no arguments (%s)" - % sys.argv) - -rules = ruleset.Ruleset.readfile(os.environ['HG_ACCESS_RULES_FILE']) -rules.set(user = getpath(os.environ['REMOTE_USER'])) -rules.set(branch = None, file = None) -todispatch = get_cmd(rules, - os.environ.get('SSH_ORIGINAL_COMMAND', '?')) -dispatch.dispatch(todispatch) - diff -r 4059dbe9f26a -r 18e93dbdaf12 hginit --- a/hginit Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -#!/bin/sh - -set -e - -cd ~hg -mkdir -p repos/hgadmin .ssh -cd repos/hgadmin -hg init . -cp /usr/local/lib/hg-admin-tools/hgadmin-hgrc .hg/hgrc - diff -r 4059dbe9f26a -r 18e93dbdaf12 install --- a/install Thu Jun 05 16:53:57 2008 +0100 +++ b/install Mon Jun 16 17:12:20 2008 +0100 @@ -4,7 +4,21 @@ install -o root -g root -d /usr/local/lib/hg-admin-tools install -o root -g root -t /usr/local/lib/hg-admin-tools \ - access.py hg-ssh refresh-auth ruleset.py hgadmin-hgrc create-breakin-repository ssh-replacement as-if-by-ssh break-in + src/access.py \ + src/hg-ssh \ + src/refresh-auth \ + src/ruleset.py +install -o root -g root -d /usr/local/lib/hg-admin-tools/init +install -o root -g root -t /usr/local/lib/hg-admin-tools/init \ + src/init/hgadmin-hgrc +install -o root -g root -d /usr/local/lib/hg-admin-tools/init/break-in +install -o root -g root -t /usr/local/lib/hg-admin-tools/init/break-in \ + src/init/break-in/create-breakin-repository \ + src/init/break-in/ssh-replacement + src/init/break-in/as-if-by-ssh + src/init/break-in/break-in install -o root -g root -d /etc/hg-admin-tools -install -o root -g root -t /etc/hg-admin-tools hg-ssh-wrapper remote-hgrc +install -o root -g root -t /etc/hg-admin-tools \ + src/init/conf/hg-ssh-wrapper \ + src/init/conf/remote-hgrc diff -r 4059dbe9f26a -r 18e93dbdaf12 refresh-auth --- a/refresh-auth Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -# WARNING -# This script completely destroys your ~/.ssh/authorized_keys -# file every time it is run -# WARNING - -import sys -import os -import os.path -import ruleset -import subprocess - -if len(sys.argv) != 3: - sys.stderr.write("refresh-auth: wrong number of arguments (%s)\n" % sys.argv) - sys.exit(-1) - -akeyfile = sys.argv[1] -wrappercommand = sys.argv[2] -prefix='no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command=' - -if os.path.exists(akeyfile): - f = open(akeyfile) - try: - for l in f: - if not l.startswith(prefix): - raise Exception("Safety check failed, delete %s to continue" % akeyfile) - finally: - f.close() - -akeys = open(akeyfile + "_new", "w") -for root, dirs, files in os.walk("keys"): - for fn in files: - ffn = os.path.join(root, fn) - if not ruleset.goodpath(ffn): - # ignore any path that contains dodgy characters - continue - keyname = ffn[5:] - if keyname == "root": - # No key can claim root privileges - continue - p = subprocess.Popen(("ssh-keygen", "-i", "-f", ffn), - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - newkey = p.communicate()[0] - if p.wait() == 0: - klines = [l.strip() for l in newkey.split("\n")] - else: - # Conversion failed, read it directly. - kf = open(ffn) - try: - klines = [l.strip() for l in kf] - finally: - kf.close() - for l in klines: - if len(l): - akeys.write('%s"%s %s" %s\n' % (prefix, wrappercommand, keyname, l)) - -akeys.close() - -os.rename(akeyfile + "_new", akeyfile) - diff -r 4059dbe9f26a -r 18e93dbdaf12 remote-hgrc --- a/remote-hgrc Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -# hgrc to use for all remote users - -[extensions] -access = /usr/local/lib/hg-admin-tools/access.py - -[hooks] -pretxnchangegroup.access = python:access.hook diff -r 4059dbe9f26a -r 18e93dbdaf12 ruleset.py --- a/ruleset.py Thu Jun 05 16:53:57 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +0,0 @@ -# Copyright 2008 LShift Ltd -# Author(s): -# Paul Crowley -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. - -import sys -import re - -allowedchars = "A-Za-z0-9_-" - -goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars)) -def goodpath(path): - return goodpathre.match(path) is not None - -goodglobre = re.compile("[*/%s]+$" % allowedchars) - -def goodglob(pattern): - return goodglobre.match(pattern) is not None - -# Don't put anything except *A-Za-z0-9_- in rule globs or -# it will match nothing. No regexp metachars, not even . -# We may fix this later. -def globmatcher(pattern): - if not goodglob(pattern): - #fail("Bad glob pattern in auth config: %s" % pattern) - # FIXME: report it somehow - return lambda x: False - # Substitution cunning so ** can be different from * - pattern = pattern.replace("*", "[]") - pattern = pattern.replace("[][]", "[/%s]*" % allowedchars) - pattern = pattern.replace("[]", "[%s]*" % allowedchars) - rex = re.compile(pattern + "$") - # None matches everything - return lambda x: x is None or rex.match(x) is not None - -def rule(pairs): - matchers = [(k, globmatcher(v)) for k, v in pairs] - def c(**kw): - for k, m in matchers: - if k not in kw or not m(kw[k]): - return False - return True - return c - -class Ruleset(object): - '''Class representing the rules in a rule file''' - - levels = ["init", "write", "read", "deny"] - - def __init__(self): - # The user called "root" automatically has the highest - # privilege - self.rules = [(self.levels[0], rule([('user', 'root')]))] - self.preset = {} - - def add(self, action, conditions): - self.rules.append((action, conditions)) - - - def set(self, **kw): - self.preset.update(kw) - - def matchrule(self, **kw): - d = self.preset.copy() - d.update(**kw) - for a, c in self.rules: - if c(**d): - return a - return None - - def allow(self, level, **kw): - a = self.matchrule(**kw) - return a in self.levels and self.levels.index(a) <= self.levels.index(level) - - @classmethod - def readfile(cls, fn): - res = cls() - try: - f = open(fn) - try: - for l in f: - l = l.strip() - if len(l) == 0 or l.startswith("#"): - continue - l = l.split() - res.add(l[0], rule([c.split("=", 1) for c in l[1:]])) - finally: - f.close() - except Exception, e: - print >> sys.stderr, "Failure reading rules file:", e - return cls() - return res - diff -r 4059dbe9f26a -r 18e93dbdaf12 src/access.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/access.py Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,55 @@ +# Copyright 2008 LShift Ltd +# Copyright 2006 Vadim Gelfer +# +# Authors: +# Paul Crowley +# Vadim Gelfer +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from mercurial.i18n import _ +from mercurial.node import * +from mercurial import util + +import os +import ruleset + +class Checker(object): + '''acl checker.''' + + def __init__(self, ui, repo): + self.ui = ui + self.repo = repo + self.rules = ruleset.Ruleset.readfile(os.environ['HG_ACCESS_RULES_FILE']) + self.rules.set(user = os.environ['REMOTE_USER']) + self.rules.set(repo = os.environ['HG_REPO_PATH']) + + def allow(self, node): + '''return if access allowed, raise exception if not.''' + ctx = self.repo.changectx(node) + branch = ctx.branch() + if not self.rules.allow("write", branch=branch, file=None): + return False + for f in ctx.files(): + if not self.rules.allow("write", branch=branch, file=f): + return False + self.ui.debug(_('%s: allowing changeset %s\n') % (__name__, short(node))) + return True + + def check(self, node): + if not self.allow(node): + raise util.Abort(_('%s: access denied for changeset %s') % + (__name__, short(node))) + + +def hook(ui, repo, hooktype, node=None, source=None, **kwargs): + if hooktype != 'pretxnchangegroup': + raise util.Abort(_('config error - hook type "%s" cannot stop ' + 'incoming changesets') % hooktype) + c = Checker(ui, repo) + start = repo.changelog.rev(bin(node)) + end = repo.changelog.count() + for rev in xrange(start, end): + c.check(repo.changelog.node(rev)) + diff -r 4059dbe9f26a -r 18e93dbdaf12 src/hg-ssh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/hg-ssh Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Copyright 2008 LShift Ltd +# Copyright 2005-2007 by Intevation GmbH +# Authors: +# Paul Crowley +# Thomas Arendsen Hein +# with ideas from Mathieu PASQUET +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +""" +hg-ssh - limit access to hg repositories reached via ssh. Part of +hg-admin-tools. + +This script is called by hg-ssh-wrapper with no arguments - everything +should be in enviroment variables: + +HG_ACCESS_RULES_FILE identifies the path to the rules file +REMOTE_USER the remote user (which is the key used by ssh) +SSH_ORIGINAL_COMMAND the command the user was trying to run + +It uses SSH_ORIGINAL_COMMAND to determine what the user was trying to +do and to what repository, and then checks each rule in the rule file +in turn for a matching rule which decides what to do, defaulting to +disallowing the action. + +""" + +# enable importing on demand to reduce startup time +from mercurial import demandimport; demandimport.enable() + +from mercurial import dispatch + +import sys, os +import ruleset + +def fail(message): + #logfile.write("Fail: %s\n" % message) + sys.stderr.write(message + "\n") + sys.exit(-1) + +def getpath(path): + if path.endswith("/"): + path = path[:-1] + if not ruleset.goodpath(path): + fail("Disallowing path: %s" % path) + return path + +def get_cmd(rules, cmd): + if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): + repo = getpath(cmd[6:-14]) + if rules.allow("read", repo=repo): + os.environ["HG_REPO_PATH"] = repo + return ['-R', repo, 'serve', '--stdio'] + elif cmd.startswith('hg init '): + repo = getpath(cmd[8:]) + if rules.allow("init", repo=repo): + os.environ["HG_REPO_PATH"] = repo + return ['init', repo] + fail("Illegal command %r" % cmd) + +#logfile = open("/tmp/hg-ssh.%d.txt" % os.getpid(), "w") +#logfile.write("Started: %s\n" % sys.argv) + +if len(sys.argv) != 1: + fail("hg-ssh must have no arguments (%s)" + % sys.argv) + +rules = ruleset.Ruleset.readfile(os.environ['HG_ACCESS_RULES_FILE']) +rules.set(user = getpath(os.environ['REMOTE_USER'])) +rules.set(branch = None, file = None) +todispatch = get_cmd(rules, + os.environ.get('SSH_ORIGINAL_COMMAND', '?')) +dispatch.dispatch(todispatch) + diff -r 4059dbe9f26a -r 18e93dbdaf12 src/init/conf/remote-hgrc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/init/conf/remote-hgrc Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,7 @@ +# hgrc to use for all remote users + +[extensions] +access = /usr/local/lib/hg-admin-tools/access.py + +[hooks] +pretxnchangegroup.access = python:access.hook diff -r 4059dbe9f26a -r 18e93dbdaf12 src/init/hginit --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/init/hginit Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +cd ~hg +mkdir -p repos/hgadmin .ssh +cd repos/hgadmin +hg init . +cp /usr/local/lib/hg-admin-tools/hgadmin-hgrc .hg/hgrc + diff -r 4059dbe9f26a -r 18e93dbdaf12 src/refresh-auth --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/refresh-auth Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# WARNING +# This script completely destroys your ~/.ssh/authorized_keys +# file every time it is run +# WARNING + +import sys +import os +import os.path +import ruleset +import subprocess + +if len(sys.argv) != 3: + sys.stderr.write("refresh-auth: wrong number of arguments (%s)\n" % sys.argv) + sys.exit(-1) + +akeyfile = sys.argv[1] +wrappercommand = sys.argv[2] +prefix='no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command=' + +if os.path.exists(akeyfile): + f = open(akeyfile) + try: + for l in f: + if not l.startswith(prefix): + raise Exception("Safety check failed, delete %s to continue" % akeyfile) + finally: + f.close() + +akeys = open(akeyfile + "_new", "w") +for root, dirs, files in os.walk("keys"): + for fn in files: + ffn = os.path.join(root, fn) + if not ruleset.goodpath(ffn): + # ignore any path that contains dodgy characters + continue + keyname = ffn[5:] + if keyname == "root": + # No key can claim root privileges + continue + p = subprocess.Popen(("ssh-keygen", "-i", "-f", ffn), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + newkey = p.communicate()[0] + if p.wait() == 0: + klines = [l.strip() for l in newkey.split("\n")] + else: + # Conversion failed, read it directly. + kf = open(ffn) + try: + klines = [l.strip() for l in kf] + finally: + kf.close() + for l in klines: + if len(l): + akeys.write('%s"%s %s" %s\n' % (prefix, wrappercommand, keyname, l)) + +akeys.close() + +os.rename(akeyfile + "_new", akeyfile) + diff -r 4059dbe9f26a -r 18e93dbdaf12 src/ruleset.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ruleset.py Mon Jun 16 17:12:20 2008 +0100 @@ -0,0 +1,95 @@ +# Copyright 2008 LShift Ltd +# Author(s): +# Paul Crowley +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import sys +import re + +allowedchars = "A-Za-z0-9_-" + +goodpathre = re.compile("([%s]+/)*[%s]+$" % (allowedchars, allowedchars)) +def goodpath(path): + return goodpathre.match(path) is not None + +goodglobre = re.compile("[*/%s]+$" % allowedchars) + +def goodglob(pattern): + return goodglobre.match(pattern) is not None + +# Don't put anything except *A-Za-z0-9_- in rule globs or +# it will match nothing. No regexp metachars, not even . +# We may fix this later. +def globmatcher(pattern): + if not goodglob(pattern): + #fail("Bad glob pattern in auth config: %s" % pattern) + # FIXME: report it somehow + return lambda x: False + # Substitution cunning so ** can be different from * + pattern = pattern.replace("*", "[]") + pattern = pattern.replace("[][]", "[/%s]*" % allowedchars) + pattern = pattern.replace("[]", "[%s]*" % allowedchars) + rex = re.compile(pattern + "$") + # None matches everything + return lambda x: x is None or rex.match(x) is not None + +def rule(pairs): + matchers = [(k, globmatcher(v)) for k, v in pairs] + def c(**kw): + for k, m in matchers: + if k not in kw or not m(kw[k]): + return False + return True + return c + +class Ruleset(object): + '''Class representing the rules in a rule file''' + + levels = ["init", "write", "read", "deny"] + + def __init__(self): + # The user called "root" automatically has the highest + # privilege + self.rules = [(self.levels[0], rule([('user', 'root')]))] + self.preset = {} + + def add(self, action, conditions): + self.rules.append((action, conditions)) + + + def set(self, **kw): + self.preset.update(kw) + + def matchrule(self, **kw): + d = self.preset.copy() + d.update(**kw) + for a, c in self.rules: + if c(**d): + return a + return None + + def allow(self, level, **kw): + a = self.matchrule(**kw) + return a in self.levels and self.levels.index(a) <= self.levels.index(level) + + @classmethod + def readfile(cls, fn): + res = cls() + try: + f = open(fn) + try: + for l in f: + l = l.strip() + if len(l) == 0 or l.startswith("#"): + continue + l = l.split() + res.add(l[0], rule([c.split("=", 1) for c in l[1:]])) + finally: + f.close() + except Exception, e: + print >> sys.stderr, "Failure reading rules file:", e + return cls() + return res +