Sharing Mercurial repositories with mercurial-server

About mercurial-server
Home page:
mercurial-server gives your developers remote read/write access to centralized Mercurial repositories using SSH public key authentication; it provides convenient and fine-grained key management and access control.
Though mercurial-server is currently targeted at Debian-based systems such as Ubuntu, other users have reported success getting it running on other Unix-based systems such as Red Hat. Running it on a non-Unix system such as Windows is not supported. You will need root privileges to install it.

Step by step
mercurial-server authenticates users not using passwords but using SSH public keys; everyone who wants access to a mercurial-server repository will need such a key. In combination with ssh-agent (or equivalents such as the Windows program Pageant), this means that users will not need to type in a password to access the repository. If you're not familiar with SSH public keys, the OpenSSH Public Key Authentication tutorial may be helpful.

Installing mercurial-server
In what follows, we assume that your username is jay, that you usually sit at a machine called my-workstation and you wish to install mercurial-server on repository-host. We assume that you have created your SSH public key, set up your SSH agent with this key, and that this key gives you access to repository-host.
First install mercurial-server on repository-host:

jay@my-workstation:~$ scp mercurial-server_0.6.1_amd64.deb repository-host:
mercurial-server_0.6.1_amd64.deb 100%
jay@my-workstation:~$ ssh -A repository-host
jay@repository-host:~$ sudo dpkg -i ../mercurial-server_0.6.1_amd64.deb
[sudo] password for jay: 
Selecting previously deselected package mercurial-server.
(Reading database ... 144805 files and directories currently installed.)
Unpacking mercurial-server (from .../mercurial-server_0.6.1_amd64.deb) ...
Setting up mercurial-server (0.6.1) ...
jay@repository-host:~$

mercurial-server is now installed on the repository host. Next, we need to give you permission to access its repositories.

jay@repository-host:~$ ssh-add -L > my-key
jay@repository-host:~$ sudo mkdir -p /etc/mercurial-server/keys/root/jay
jay@repository-host:~$ sudo cp my-key /etc/mercurial-server/keys/root/jay/my-workstation
jay@repository-host:~$ sudo -u hg /usr/share/mercurial-server/refresh-auth
jay@repository-host:~$ exit
Connection to shell closed.
jay@my-workstation:~$

You can now create repositories on the remote machine and have complete read-write access to all of them.

Creating repositories
To store a repository on the server, clone it over.

jay@my-workstation:~$ cd my-mercurial-project
jay@my-workstation:~/my-mercurial-project$ hg clone . ssh://hg@repository-host/repository/name
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 119 changesets with 284 changes to 61 files
jay@my-workstation:~/my-mercurial-project$ hg pull ssh://hg@repository-host/repository/name
pulling from ssh://hg@repository-host/repository/name
searching for changes
no changes found
jay@my-workstation:~/my-mercurial-project$ cd ..
jay@my-workstation:~$

Adding other users
As things stand, no-one but you has any access to any repositories you create on this system. In order to give anyone else access, you'll need a copy of their SSH public key; we'll assume you have that key in ~/sam-key.pub. To manage access, you make changes to the special hgadmin repository.

jay@my-workstation:~$ hg clone ssh://hg@repository-host/hgadmin
destination directory: hgadmin
no changes found
updating working directory
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
jay@my-workstation:~$ cd hgadmin
jay@my-workstation:~/hgadmin$ mkdir -p keys/users/sam
jay@my-workstation:~/hgadmin$ cp ~/sam-key.pub keys/users/sam/their-workstation
jay@my-workstation:~/hgadmin$ hg add
adding keys/users/sam/their-workstation
jay@my-workstation:~/hgadmin$ hg commit -m "Add Sam's key'"
jay@my-workstation:~/hgadmin$ hg push
pushing to ssh://hg@repository-host/hgadmin
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files
jay@my-workstation:~/hgadmin$

Sam can now read and write to your ssh://hg@repository-host/repository/name repository. Most other changes to access control can be made simply by making and pushing changes to hgadmin, and you can use Mercurial to cooperate with other root users in the normal way.
If you prefer, you could give them access by logging into repository-host, putting the key in the right place under /etc/mercurial-server/keys, and re-running sudo -u hg /usr/share/mercurial-server/refresh-auth. However, using hgadmin is usually more convenient if you need to make more than a very few changes; it also makes it easier to share administration with others and provides a log of all changes.

Access control
Out of the box, mercurial-server supports two kinds of users: "root" users and normal users. If you followed the steps above, you are a "root" user because your key is under keys/root, while the other user you gave access to is a normal user since their key is under keys/users. Keys that are not in either of these directories will by default have no access to anything.
Root users can edit hgadmin, create new repositories and read and write to existing ones. Normal users cannot access hgadmin or create new repositories, but they can read and write to any other repository.

Using access.conf
mercurial-server offers much more fine-grained access control than this division into two classes of users. Let's suppose you wish to give Pat access to the widget repository, but no other. We first copy Pat's SSH public key into the keys/widget/pat directory in hgadmin. Now mercurial-server knows about Pat's key, but will give Pat no access to anything because the key is not under either keys/root or keys/users. To grant this key access, we must give mercurial-server a new access rule, so we create a file in hgadmin called access.conf, with the following contents:

    write repo=widget user=widget/**

Pat will have read and write access as soon as we add, commit, and push these files.
Each line of access.conf has the following syntax:

rule condition condition...

Blank lines and lines that start with # are ignored. Rule is one of
init: allow reads, writes, and the creation of new repositories
write: allow reads and writes
read: allow only read operations
deny: deny all requests
When considering a request, mercurial-server steps through all the rules in /etc/mercurial-server/access.conf and then all the rules in access.conf in hgadmin looking for a rule which matches on every condition. If it does not find such a rule, it denies the request; otherwise it checks whether the rule grants sufficient privilege to allow it.
By default, /etc/mercurial-server/access.conf has the following rules:

    init user=root/**
    deny repo=hgadmin
    write user=users/**

These rules ensure that root users can do any operation on any repository, that no other users can access the hgadmin repository, and that those with keys in keys/users can read or write to any repository but not create repositories.
A condition is a globpattern matched against a relative path. The two most important conditions are
user=globpattern: path to the user's key
repo=globpattern: path to the repository
"*" 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 "/".

/etc/mercurial-server and hgadmin
mercurial-server consults two distinct locations to collect information about what to allow: /etc/mercurial-server and its own hgadmin repository. This is useful for several reasons:
Users may not need the sophistication of access control via mercurial; for these users updating /etc/mercurial-server may offer a simpler route.
/etc/mercurial-server is suitable for management by some other route, such as with Puppet
If a change to hgadmin leaves you "locked out", /etc/mercurial-server allows you a way back in.
At install time, all users are "locked out", and so some mechanism to allow some users in is needed.
Rules in /etc/mercurial-server/access.conf take precedence over those in hgadmin, and obviously keys in /etc/mercurial-server/keys cannot be affected by changes to hgadmin.
We anticipate that once mercurial-server is successfully installed and working most users will want to use hgadmin for most access control tasks. Once you have the right keys and access.conf set up in hgadmin, you can delete /etc/mercurial-server/access.conf and all of /etc/mercurial-server/keys, turning control entirely over to hgadmin.
/etc/mercurial-server/remote-hgrc is in the HGRCPATH for all remote access to mercurial-server repositories. This file contains the hooks that mercurial-server uses for access control and logging. You can add hooks to this file, but obviously breaking the existing hooks will disable the relevant functionality and isn't advisable.

File and branch conditions
mercurial-server supports file and branch conditions, which restrict an operation depending on what files it modifies and what branch the work is on.
The way these conditions work is subtle and can be counterintuitive. Unless you need what they provide, ignore this section, 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:
file=globpattern: file within the repo
branch=globpattern: Mercurial branch name
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 three decisions:
Whether to allow a repository to be created
Whether to allow any access to a repository
Whether to allow a changeset, which is on a some branch
Whether to allow a changeset which changes 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 and branch 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:

   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.
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:

   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:

  write user=docs/* branch=docs
  write user=docs/* file=docs/*
  read user=docs/*

How mercurial-server works
All of the repositories controlled by mercurial-server are owned by a single user, the hg user, which is why all URLs for mercurial-server repositories start with ssh://hg@.... Each SSH key that has access to the repository has an entry in ~hg/.ssh/authorized_keys; this is how the SSH daemon knows to give that key access. When the user connects over SSH, their commands are run in a specially crafted restricted shell; this shell knows which key was used to connect, determines what the user is trying to do, and checks the access rules to decide whether to allow it.
This restricted shell also ensures that certain Mercurial extensions are loaded when the user acts on a repository; these extensions check the access control rules for any changeset that the user tries to commit, and log all pushes and pulls into a per-repository access log.
refresh-auth recurses through the /etc/mercurial-server/keys and the keys directory in the hgadmin repository, creating an entry in ~hg/.ssh/authorized_keys for each one. This is redone automatically whenever a change is pushed to hgadmin.

Security
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. Any attack on mercurial-server can only be started if the attacker already has 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. 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 short, mercurial-server is a fairly new program and may harbour bugs. Backups are essential!

Legalese
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Thanks
Thanks for reading this far. If you use mercurial-server, please tell me about it.
Paul Crowley, paul@lshift.net, 2009