# HG changeset patch # User Paul Crowley # Date 1255444203 -3600 # Node ID 731a72b742db7880483099beb592e0e4b3fe1a91 # Parent e99262dfa9507b605235dd001fe7b69c8f2783c1# Parent 241475f6440ca676b858edd9a6b12e75188eb873 get up to date diff -r e99262dfa950 -r 731a72b742db .hgtags --- a/.hgtags Thu May 28 10:43:30 2009 +0100 +++ b/.hgtags Tue Oct 13 15:30:03 2009 +0100 @@ -1,1 +1,2 @@ 535502c18eaad098437e49adea1e26a68e4b6d75 release_0.5 +975fb921c3f3ffe7ccde5877f2954a5d1141bb14 release_0.6 diff -r e99262dfa950 -r 731a72b742db README --- a/README Thu May 28 10:43:30 2009 +0100 +++ b/README Tue Oct 13 15:30:03 2009 +0100 @@ -4,7 +4,7 @@ you choose, identified by ssh keys, with easy key and access management based on hg. -http://hg.opensource.lshift.net/mercurial-server/ +http://www.lshift.net/mercurial-server.html Copyright (C) 2008-2009 LShift Ltd. @@ -40,7 +40,7 @@ To give a user access to the repository, place their key in an appropriately-named subdirectory of "/etc/mercurial-server/keys" and run -"/usr/local/lib/mercurial-server/refresh-auth". You can then control what +"/usr/local/share/mercurial-server/refresh-auth". You can then control what access they have to what repositories by editing the control file "/etc/mercurial-server/access.conf", which can match the names of these keys against a glob pattern. @@ -75,7 +75,7 @@ (ie the file is called something like "/etc/mercurial-server/keys/root/yourname/yourhostname") so that you can easily manage users who have a different key on each host they use. Then run -"/usr/local/lib/mercurial-server/refresh-auth". +"/usr/local/share/mercurial-server/refresh-auth". The repository is now ready to use, and you are now the sole user able to change and create repositories on this repository host. @@ -98,7 +98,7 @@ "keys/users" subdirectory - these users will be able to read and write to any repository (except one - see below) but will not be able to create new repositories. As always, when you change "/etc/mercurial-server/keys" you need -to re-run "/usr/local/lib/mercurial-server/refresh-auth". +to re-run "/usr/local/share/mercurial-server/refresh-auth". LOGGING @@ -151,4 +151,5 @@ Thanks for reading this far. If you use mercurial-server, please tell me about it. -Paul Crowley, 2009 +Paul Crowley, paul@lshift.net, 2009 + diff -r e99262dfa950 -r 731a72b742db doc/PLAN --- a/doc/PLAN Thu May 28 10:43:30 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -Plan: do away with shell scripts, and un-hard-wire the path /usr/local/lib/mercurial-server. - -Done: - -- move all modules into mercurial_server directory -- make mercurial_server.paths module; a function sets a global based on sys.argv[0] -- reduce hg-ssh-wrapper to nothing by moving all functionality into hg-ssh -- same for refresh-auth -- move most of do-refresh-auth into a module -- give that module a hook, and call the hook instead of the exe -- abolish refresh-auth shell script and rename do-refresh-auth to refresh-auth -- replace env vars with Python globals -- change refresh-auth to refer to hg-ssh directly, abolish hg-ssh-wrapper - -Todo: - -- add comment check and -f flag to refresh-auth -- change install script to a Python script, abolishing hginit - - diff -r e99262dfa950 -r 731a72b742db doc/configuring-access --- a/doc/configuring-access Thu May 28 10:43:30 2009 +0100 +++ b/doc/configuring-access Tue Oct 13 15:30:03 2009 +0100 @@ -1,10 +1,10 @@ ACCESS.CONF Out of the box, there are just two kinds of users: the ones with keys in -"keys/root" and those in "keys/users". However, you can change this by editing -"access.conf". There are two "access.conf" files, one in -"/etc/mercurial-server" and one in "hgadmin"; the two are simply concatenated -before being read. +"keys/root" and those in "keys/users". However, you can change this by +editing "access.conf". There are two "access.conf" files, one in +"/etc/mercurial-server" and one in "hgadmin"; the two are simply +concatenated before being read. Each line of access.conf has the following syntax: @@ -23,13 +23,13 @@ user= - user's key repo= - repo (as the user supplies it) -The first rule in the file which has all its conditions satisfied is used to -determine whether an action is allowed. +The first rule in the file which has all its conditions satisfied is used +to determine whether an action is allowed. If no rule is matched the +request is denied. -Paths cannot contain any special characters except "/"; glob patterns cannot -contain any special characters except "/" and "*". "*" matches zero or more -characters not including "/" while "**" matches zero or more characters -including "/". +"*" only matches one directory level, where "**" matches as many as you +want. More precisely, "*" matches zero or more characters not including "/" +while "**" matches zero or more characters including "/". Blank lines and lines that start with "#" are ignored. @@ -40,8 +40,8 @@ write user=users/** This means: keys in "root" can do anything; keys in "users" cannot create -repositories, cannot even read the hgadmin repository, but can read and write -any other repository; no other key has any access. +repositories, cannot even read the hgadmin repository, but can read and +write any other repository; no other key has any access. More advanced access configuration is covered in file-conditions. diff -r e99262dfa950 -r 731a72b742db doc/file-conditions --- a/doc/file-conditions Thu May 28 10:43:30 2009 +0100 +++ b/doc/file-conditions Tue Oct 13 15:30:03 2009 +0100 @@ -3,10 +3,11 @@ Read configuring-access before you read this. mercurial-server supports file and branch conditions, which restrict an -operation depending on what files it modifies and what branch the work is on. -However, the way these conditions work is subtle and can be counterintuitive - -if you want to keep things simple, stick to user and repo conditions, and then -things are likely to work the way you would expect. +operation depending on what files it modifies and what branch the work is +on. However, the way these conditions work is subtle and can be +counterintuitive - if you want to keep things simple, stick to user and +repo conditions, and then things are likely to work the way you would +expect. File and branch conditions are added to the conditions against which a rule matches, just like user and repo conditions; they have this form: @@ -14,8 +15,8 @@ file= - file in the repo branch= - name of the branch -However, in order to understand what effect adding these conditions will have, -it helps to understand how and when these rules are applied. +However, in order to understand what effect adding these conditions will +have, it helps to understand how and when these rules are applied. The rules file is used to make four decisions: @@ -24,35 +25,35 @@ - Whether to allow a changeset on a particular branch at all - Whether to allow a changeset to change a particular file -When the first two of these decisions are being made, nothing is known about -what files might be changed, and so all file conditions automatically succeed -for the purpose of such decisions. This means that doing tricky things with -file conditions can have counterintuitive consequences: +When the first two of these decisions are being made, nothing is known +about what files might be changed, and so all file conditions automatically +succeed for the purpose of such decisions. This means that doing tricky +things with file conditions can have counterintuitive consequences: -- You cannot limit read access to a subset of a repository with a "read" rule -and a file condition: any user who has access to a repository can read all of -it and its full history. Such a rule can only have the effect of masking a -later "write" rule, as in this example: +- You cannot limit read access to a subset of a repository with a "read" +rule and a file condition: any user who has access to a repository can read +all of it and its full history. Such a rule can only have the effect of +masking a later "write" rule, as in this example: read repo=specialrepo file=dontwritethis write repo=specialrepo -allows all users to read specialrepo, and to write to all files *except* that -any changeset which writes to "dontwritethis" will be rejected. +allows all users to read specialrepo, and to write to all files *except* +that any changeset which writes to "dontwritethis" will be rejected. - For similar reasons, don't give "init" rules file conditions. -- Don't try to deny write access to a particular file on a particular branch - -a developer can write to the file on another branch and then merge it in. -Either deny all writes to the branch from that user, or allow them to write to -all the files they can write to on any branch. In other words, something like -this will have the intended effect: +- Don't try to deny write access to a particular file on a particular +branch - a developer can write to the file on another branch and then merge +it in. Either deny all writes to the branch from that user, or allow them +to write to all the files they can write to on any branch. In other words, +something like this will have the intended effect: write user=docs/* branch=docs file=docs/* -But something like this will not have the intended effect; it will effectively -allow these users to write to any file on any branch, by writing it to "docs" -first: +But something like this will not have the intended effect; it will +effectively allow these users to write to any file on any branch, by +writing it to "docs" first: write user=docs/* branch=docs write user=docs/* file=docs/* diff -r e99262dfa950 -r 731a72b742db doc/how-it-works --- a/doc/how-it-works Thu May 28 10:43:30 2009 +0100 +++ b/doc/how-it-works Tue Oct 13 15:30:03 2009 +0100 @@ -1,22 +1,24 @@ HOW IT WORKS -When a developer attempts to connect to a repository via ssh, the SSH daemon -searches for a match for that user's key in ~hg/.ssh/authorized_keys. If the -developer is authorised to connect to the repository they will have an entry -in this file. The entry includes a "command" prefix which specifies that the -restricted shell "/usr/local/lib/mercurial-server/hg-ssh" should be used; this -shell is passed an argument identifying the developer. The shell parses the -command the developer is trying to execute, and consults a rules file to see -if that developer is allowed to perform that action on that repository. +When a developer attempts to connect to a repository via ssh, the SSH +daemon searches for a match for that user's key in +~hg/.ssh/authorized_keys. If the developer is authorised to connect to the +repository they will have an entry in this file. The entry includes a +"command" prefix which specifies that the restricted shell +"/usr/local/share/mercurial-server/hg-ssh" should be used; this shell is +passed an argument identifying the developer. The shell parses the command +the developer is trying to execute, and consults a rules file to see if +that developer is allowed to perform that action on that repository. The file ~hg/.ssh/authorized_keys is generated by "refresh-auth", which -recurses through two directories of files containing SSH keys and generates an -entry in authorized_keys for each one, using the name of the key file as the -identifier for the developer. These keys will live in the "keys" subdirectory -"/etc/mercurial-server" and the "keys" subdirectory of a repository called -"hgadmin". A hook in this repository re-runs "refresh-auth" on the most recent -version after every push. +recurses through two directories of files containing SSH keys and generates +an entry in authorized_keys for each one, using the name of the key file as +the identifier for the developer. These keys will live in the "keys" +subdirectory "/etc/mercurial-server" and the "keys" subdirectory of a +repository called "hgadmin". A hook in this repository re-runs +"refresh-auth" on the most recent version after every push. -Finally, hook in an extension is run for each changeset that is remotely -committed, which uses the rules file to determine whether to allow the -changeset. +When users try to commit new changesets, a hook is run which consults the +rules file to decide whether to allow the changeset into the repository. +This can depend not only on the user and the repository, but also the +branch and files in the changeset. diff -r e99262dfa950 -r 731a72b742db doc/security --- a/doc/security Thu May 28 10:43:30 2009 +0100 +++ b/doc/security Tue Oct 13 15:30:03 2009 +0100 @@ -1,21 +1,21 @@ SECURITY OF MERCURIAL-SERVER -mercurial-server relies entirely on sshd to grant access to remote users. As a -result, it runs no daemons, installs no setuid programs, and no part of it -runs as root except the install process: all programs run as the user hg. And -any attack on mercurial-server can only be started if the Bad Guys already -have a public key in ~hg/.ssh/authorized_keys, otherwise sshd will bar the -way. No matter what command the user tries to run on the remote system via -ssh, mercurial-server is run. +mercurial-server relies entirely on sshd to grant access to remote users. +As a result, it runs no daemons, installs no setuid programs, and no part +of it runs as root except the install process: all programs run as the user +hg. And any attack on mercurial-server can only be started if the Bad Guys +already have a public key in ~hg/.ssh/authorized_keys, otherwise sshd will +bar the way. -It parses the command line the user asked for, and interprets and runs the -corresponding hg operation itself if access is allowed, so users can only read -and add to history within repositories; they cannot run any other hg command. -In addition, every push and pull is logged with a datestamp, changeset ID and -the key that performed the operation. +No matter what command the user tries to run on the remote system via ssh, +mercurial-server is run. It parses the command line the user asked for, and +interprets and runs the corresponding hg operation itself if access is +allowed, so users can only read and add to history within repositories; +they cannot run any other hg command. In addition, every push and pull is +logged with a datestamp, changeset ID and the key that performed the +operation. -However, while the first paragraph holds no matter what bugs mercurial-server -contains, the second depends on the relevant code being correct; though the -entire codebase is currently only about twice as long as this README, -mercurial-server is a fairly new program and may harbour bugs. Backups are -essential! +However, while the first paragraph holds no matter what bugs +mercurial-server contains, the second depends on the relevant code being +correct; though the entire codebase is short, mercurial-server is a fairly +new program and may harbour bugs. Backups are essential! diff -r e99262dfa950 -r 731a72b742db src/hg-ssh --- a/src/hg-ssh Thu May 28 10:43:30 2009 +0100 +++ b/src/hg-ssh Tue Oct 13 15:30:03 2009 +0100 @@ -33,45 +33,53 @@ from mercurial import dispatch -import sys, os +import sys, os, os.path +import base64 from mercurialserver import ruleset, paths def fail(message): - #logfile.write("Fail: %s\n" % message) - sys.stderr.write(message + "\n") + sys.stderr.write("mercurial-server: %s\n" % message) 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 checkDots(path): + head, tail = os.path.split(path) + if tail.startswith("."): + fail("paths cannot contain dot file components") + if head: + checkDots(head) + +def checkParents(path): + path = os.path.dirname(path) + if path == "": + return + if os.path.exists(path + "/.hg"): + fail("Cannot create repo under existing repo") + checkParents(path) -def try_cmd(cmd): - if cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): - repo = getpath(cmd[6:-14]) - ruleset.rules.set(repo=repo) - if ruleset.rules.allow("read", branch=None, file=None): - dispatch.dispatch(['-R', repo, 'serve', '--stdio']) - return - elif cmd.startswith('hg init '): - repo = getpath(cmd[8:]) - ruleset.rules.set(repo=repo) - if ruleset.rules.allow("init", branch=None, file=None): - dispatch.dispatch(['init', repo]) - return - 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) != 2: - fail("hg-ssh must have exactly one argument (%s)" - % sys.argv) +def getrepo(op, repo): + # First canonicalise, then check the string, then the rules + # and finally the filesystem. + repo = repo.rstrip("/") + if len(repo) == 0: + fail("path to repository seems to be empty") + if repo.startswith("/"): + fail("absolute paths are not supported") + checkDots(path) + ruleset.rules.set(repo=repo) + if not ruleset.rules.allow(op, branch=None, file=None): + fail("access denied") + checkParents(repo) + return repo paths.setExePath() -ruleset.rules.set(user = sys.argv[1]) + +if len(sys.argv) == 3 and sys.argv[1] == "--base64": + ruleset.rules.set(user = base64.b64decode(sys.argv[2])) +elif len(sys.argv) == 2: + ruleset.rules.set(user = sys.argv[1]) +else: + fail("hg-ssh wrongly called, is authorized_keys corrupt? (%s)" + % sys.argv) # Use a different hgrc for remote pulls - this way you can set # up access.py for everything at once without affecting local operations @@ -86,4 +94,22 @@ if os.path.isfile(f): ruleset.rules.readfile(f) -try_cmd(os.environ.get('SSH_ORIGINAL_COMMAND', '?')) +cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None) +if cmd is None: + fail("direct logins on the hg account prohibited") +elif cmd.startswith('hg -R ') and cmd.endswith(' serve --stdio'): + repo = getrepo("read", cmd[6:-14]) + if not os.path.isdir(repo + "/.hg"): + fail("no such repository %s" % repo) + dispatch.dispatch(['-R', repo, 'serve', '--stdio']) +elif cmd.startswith('hg init '): + repo = getrepo("init", cmd[8:]) + if os.path.exists(repo): + fail("%s exists" % repo) + d = os.path.dirname(repo) + if d != "" and not os.path.isdir(d): + os.makedirs(d) + dispatch.dispatch(['init', repo]) +else: + fail("illegal command %r" % cmd) + diff -r e99262dfa950 -r 731a72b742db src/init/hgadmin-hgrc --- a/src/init/hgadmin-hgrc Thu May 28 10:43:30 2009 +0100 +++ b/src/init/hgadmin-hgrc Tue Oct 13 15:30:03 2009 +0100 @@ -1,6 +1,9 @@ # WARNING: when these hooks run they will entirely destroy and rewrite # ~/.ssh/authorized_keys +[extensions] +hgext.purge = + [hooks] changegroup.aaaab_update = hg update -C default > /dev/null changegroup.aaaac_purge = hg purge --all > /dev/null diff -r e99262dfa950 -r 731a72b742db src/mercurialserver/refreshauth.py --- a/src/mercurialserver/refreshauth.py Thu May 28 10:43:30 2009 +0100 +++ b/src/mercurialserver/refreshauth.py Tue Oct 13 15:30:03 2009 +0100 @@ -5,12 +5,15 @@ # file every time it is run # WARNING -import sys +import re +import base64 import os import os.path import pwd import subprocess -from mercurialserver import ruleset, paths +from mercurialserver import paths + +goodkey = re.compile("[/A-Za-z0-9._-]+$") def refreshAuth(pw_dir): akeyfile = pw_dir + "/.ssh/authorized_keys" @@ -38,10 +41,9 @@ raise Exception("Inconsistent behaviour in os.walk, bailing") #print "Processing file", ffn keyname = ffn[len(kr):] - if not ruleset.goodpath(keyname): - # ignore any path that contains dodgy characters - #print "Ignoring file", ffn - continue + if not goodkey.match(keyname): + # Encode it for safe quoting + keyname = "--base64 " + base64.b64encode(keyname) p = subprocess.Popen(("ssh-keygen", "-i", "-f", ffn), stdout=subprocess.PIPE, stderr=subprocess.PIPE) newkey = p.communicate()[0] diff -r e99262dfa950 -r 731a72b742db src/mercurialserver/ruleset.py --- a/src/mercurialserver/ruleset.py Thu May 28 10:43:30 2009 +0100 +++ b/src/mercurialserver/ruleset.py Tue Oct 13 15:30:03 2009 +0100 @@ -12,28 +12,10 @@ 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 + "$") + p = "[^/]*".join(re.escape(c) for c in pattern.split("*")) + # ** means "match recursively" ie "ignore directories" + rex = re.compile(p.replace("[^/]*[^/]*", ".*") + "$") # None matches everything return lambda x: x is None or rex.match(x) is not None