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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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__":