2
 
###############################################################################
 
5
 
#    Simple access control for shared bazaar repository accessed over ssh.
 
7
 
# Copyright (C) 2007 Balint Aradi
 
9
 
# This program is free software; you can redistribute it and/or modify
 
10
 
# it under the terms of the GNU General Public License as published by
 
11
 
# the Free Software Foundation; either version 2 of the License, or
 
12
 
# (at your option) any later version.
 
14
 
# This program is distributed in the hope that it will be useful,
 
15
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
 
# GNU General Public License for more details.
 
19
 
# You should have received a copy of the GNU General Public License
 
20
 
# along with this program; if not, write to the Free Software
 
21
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
23
 
###############################################################################
 
25
 
Invocation: bzr_access <bzr_executable> <repo_collection> <user>
 
27
 
The script extracts from the SSH_ORIGINAL_COMMAND environment variable the
 
28
 
repository, which bazaar tries to access through the bzr+ssh protocol. The
 
29
 
repository is assumed to be relative to <repo_collection>. Based
 
30
 
on the configuration file <repo_collection>/bzr_access.conf it determines
 
31
 
the access rights (denied, read-only, read-write) for the specified user.
 
32
 
If the user has read-only or read-write access a bazaar smart server is
 
33
 
started for it in read-only or in read-write mode, rsp., using the specified
 
36
 
Config file: INI format, pretty much similar to the authfile of subversion.
 
38
 
Groups can be defined in the [groups] section. The options in this block are
 
39
 
the names of the groups to be defined, the corresponding values the lists of
 
40
 
the users belonging to the given groups. (User names must be separated by
 
43
 
Right now only one section is supported [/], defining the permissions for the
 
44
 
repository. The options in those sections are user names or group references
 
45
 
(group name with a leading '@'), the corresponding values are the 
 
46
 
permissions: 'rw', 'r' and '' (without the quotes)
 
47
 
for read-write, read-only and no access, respectively.
 
49
 
Sample bzr_access.conf::
 
53
 
   devels = beta, gamma, delta
 
59
 
This allows you to set up a single SSH user, and customize the access based on
 
60
 
ssh key. Your ``.ssh/authorized_key`` file should look something like this::
 
62
 
   command="/path/to/bzr_access /path/to/bzr /path/to/repository <username>",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-<type> <key>
 
71
 
CONFIG_FILE = "bzr_access.conf"
 
72
 
SCRIPT_NAME = os.path.basename(sys.argv[0])
 
74
 
# Permission constants
 
78
 
PERM_DICT = { "r": PERM_READ, "rw": PERM_READWRITE }
 
90
 
# pattern for the bzr command passed to ssh
 
91
 
PAT_SSH_COMMAND = re.compile(r"""^bzr\s+
 
94
 
                             --directory=(?P<dir>\S+)\s+
 
95
 
                             --allow-writes\s*$""", re.VERBOSE)
 
97
 
# Command line for starting bzr
 
98
 
BZR_OPTIONS = ['serve', '--inet', '--directory']
 
99
 
BZR_READWRITE_FLAGS = ['--allow-writes']
 
103
 
def error(msg, exit_code):
 
104
 
    """Prints error message to stdout and exits with given error code."""
 
106
 
    print >>sys.stderr, "%s::error: %s" % (SCRIPT_NAME, msg)
 
111
 
class AccessManager(object):
 
112
 
    """Manages the permissions, can be queried for a specific user and path."""
 
114
 
    def __init__(self, fp):
 
115
 
        """:param fp: File like object, containing the configuration options.
 
117
 
        # TODO: jam 20071211 Consider switching to bzrlib.util.configobj
 
118
 
        self.config = ConfigParser.ConfigParser()
 
119
 
        self.config.readfp(fp)
 
121
 
        if self.config.has_section("groups"):
 
122
 
            for group, users in self.config.items("groups"):
 
123
 
                self.groups[group] = set([ s.strip() for s in users.split(",")])
 
126
 
    def permission(self, user):
 
127
 
        """Determines the permission for a given user and a given path
 
128
 
        :param user: user to look for.
 
133
 
        pathFound = self.config.has_section(configSection)
 
135
 
            options = reversed(self.config.options(configSection))
 
136
 
            for option in options:
 
137
 
                value = PERM_DICT.get(self.config.get(configSection, option),
 
139
 
                if self._is_relevant(option, user):
 
144
 
    def _is_relevant(self, option, user):
 
145
 
        """Decides if a certain option is relevant for a given user.
 
147
 
        An option is relevant if it is identical with the user or with a
 
148
 
        reference to a group including the user.
 
150
 
        :param option: Option to check.
 
152
 
        :return: True if option is relevant for the user, False otherwise.
 
154
 
        if option.startswith("@"):
 
155
 
            result = (user in self.groups.get(option[1:], set()))
 
157
 
            result = (option == user)
 
162
 
def get_directory(command):
 
163
 
    """Extracts the directory name from the command pass to ssh.
 
164
 
    :param command: command to parse.
 
165
 
    :return: Directory name or empty string, if directory was not found or if it
 
166
 
    does not start with '/'.
 
168
 
    match = PAT_SSH_COMMAND.match(command)
 
171
 
    directory = match.group("dir")
 
172
 
    return os.path.normpath(directory)
 
176
 
############################################################################
 
178
 
############################################################################
 
181
 
    if len(sys.argv) != 4:
 
182
 
        error("Invalid number or arguments.", EXIT_BAD_NR_ARG)
 
183
 
    (bzrExec, repoRoot, user) = sys.argv[1:4]
 
186
 
    if not os.access(bzrExec, os.X_OK):
 
187
 
        error("bzr is not executable.", EXIT_BZR_NOEXEC)
 
188
 
    if not os.access(repoRoot, os.R_OK):
 
189
 
        error("Path to repository not readable.", EXIT_REPO_NOREAD)
 
191
 
    # Extract the repository path from the command passed to ssh.
 
192
 
    if not os.environ.has_key("SSH_ORIGINAL_COMMAND"):
 
193
 
        error("Environment variable SSH_ORIGINAL_COMMAND missing.", EXIT_BADENV)
 
194
 
    directory = get_directory(os.environ["SSH_ORIGINAL_COMMAND"])
 
195
 
    if len(directory) == 0:
 
196
 
        error("Bad directory name.", EXIT_BADDIR)
 
199
 
    if not user.isalnum():
 
200
 
        error("Invalid user name", EXIT_BADUSERNAME)
 
202
 
    # Read in config file.
 
204
 
        fp = open(os.path.join(repoRoot, CONFIG_FILE), "r")
 
206
 
            accessMan = AccessManager(fp)
 
210
 
        error("Can't read config file.", EXIT_NOCONF)
 
212
 
    # Determine permission and execute bzr with appropriate options
 
213
 
    perm = accessMan.permission(user)
 
214
 
    command = [bzrExec] + BZR_OPTIONS + [repoRoot]
 
215
 
    if perm == PERM_READ:
 
216
 
        # Nothing extra needed for readonly operations
 
218
 
    elif perm == PERM_READWRITE:
 
219
 
        # Add the write flags
 
220
 
        command.extend(BZR_READWRITE_FLAGS)
 
222
 
        error("Access denied.", EXIT_NOACCESS)
 
223
 
    return subprocess.call(command)
 
226
 
if __name__ == "__main__":