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
All other sections names should be path names (starting with '/'), defining
44
the permissions for the given path. The options in those sections are user
45
names or group references (group name with a leading '@'), the corresponding
46
values are the permissions: 'rw', 'r' and '' (without the quotes) for
47
read-write, read-only and no access, respectively.
49
Only the options in the section with the longest matching name are evaluated.
50
The last relevant option for the user is used.
52
Sample bzr_access.conf::
56
devels = beta, gamma, delta
67
This allows you to set up a single SSH user, and customize the access based on
68
ssh key. Your ``.ssh/authorized_key`` file should look something like this::
70
command="/path/to/bzr_access /path/to/bzr /path/to/repository <username>",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-<type> <key>
79
CONFIG_FILE = "bzr_access.conf"
80
SCRIPT_NAME = os.path.basename(sys.argv[0])
82
# Permission constants
86
PERM_DICT = { "r": PERM_READ, "rw": PERM_READWRITE }
98
# pattern for the bzr command passed to ssh
99
PAT_SSH_COMMAND = re.compile(r"""^bzr\s+
102
--directory=(?P<dir>\S+)\s+
103
--allow-writes\s*$""", re.VERBOSE)
105
# Command line for starting bzr
106
BZR_OPTIONS = ['serve', '--inet', '--directory']
107
BZR_READWRITE_FLAGS = ['--allow-writes']
111
def error(msg, exit_code):
112
"""Prints error message to stdout and exits with given error code."""
114
print >>sys.stderr, "%s::error: %s" % (SCRIPT_NAME, msg)
119
class AccessManager(object):
120
"""Manages the permissions, can be queried for a specific user and path."""
122
def __init__(self, fp):
123
""":param fp: File like object, containing the configuration options.
125
# TODO: jam 20071211 Consider switching to bzrlib.util.configobj
126
self.config = ConfigParser.ConfigParser()
127
self.config.readfp(fp)
129
if self.config.has_section("groups"):
130
for group, users in self.config.items("groups"):
131
self.groups[group] = set([ s.strip() for s in users.split(",")])
134
def permission(self, user, path):
135
"""Determines the permission for a given user and a given path
136
:param user: user to look for.
137
:param path: path to look for.
140
if not path.startswith("/"):
144
while not pathFound and path != "/":
145
print >>sys.stderr, "DEBUG:", path
146
pathFound = self.config.has_section(path)
148
options = reversed(self.config.options(path))
149
for option in options:
150
value = PERM_DICT.get(self.config.get(path, option),
152
if self._is_relevant(option, user):
155
path = os.path.dirname(path)
159
def _is_relevant(self, option, user):
160
"""Decides if a certain option is relevant for a given user.
162
An option is relevant if it is identical with the user or with a
163
reference to a group including the user.
165
:param option: Option to check.
167
:return: True if option is relevant for the user, False otherwise.
169
if option.startswith("@"):
170
result = (user in self.groups.get(option[1:], set()))
172
result = (option == user)
177
def get_directory(command):
178
"""Extracts the directory name from the command pass to ssh.
179
:param command: command to parse.
180
:return: Directory name or empty string, if directory was not found or if it
181
does not start with '/'.
183
match = PAT_SSH_COMMAND.match(command)
186
directory = match.group("dir")
187
return os.path.normpath(directory)
191
############################################################################
193
############################################################################
196
if len(sys.argv) != 4:
197
error("Invalid number or arguments.", EXIT_BAD_NR_ARG)
198
(bzrExec, repoRoot, user) = sys.argv[1:4]
201
if not os.access(bzrExec, os.X_OK):
202
error("bzr is not executable.", EXIT_BZR_NOEXEC)
203
if not os.access(repoRoot, os.R_OK):
204
error("Path to repository not readable.", EXIT_REPO_NOREAD)
206
# Extract the repository path from the command passed to ssh.
207
if not os.environ.has_key("SSH_ORIGINAL_COMMAND"):
208
error("Environment variable SSH_ORIGINAL_COMMAND missing.", EXIT_BADENV)
209
directory = get_directory(os.environ["SSH_ORIGINAL_COMMAND"])
210
if len(directory) == 0:
211
error("Bad directory name.", EXIT_BADDIR)
214
if not user.isalnum():
215
error("Invalid user name", EXIT_BADUSERNAME)
217
# Read in config file.
219
fp = open(os.path.join(repoRoot, CONFIG_FILE), "r")
221
accessMan = AccessManager(fp)
225
error("Can't read config file.", EXIT_NOCONF)
227
# Determine permission and execute bzr with appropriate options
228
perm = accessMan.permission(user, directory)
229
absDir = os.path.join(repoRoot, directory)
230
command = [bzrExec] + BZR_OPTIONS + [absDir]
231
if perm == PERM_READ:
232
# Nothing extra needed for readonly operations
234
elif perm == PERM_READWRITE:
235
# Add the write flags
236
command.extend(BZR_READWRITE_FLAGS)
238
error("Access denied.", EXIT_NOACCESS)
239
return subprocess.call(command)
242
if __name__ == "__main__":