~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to contrib/bzr_access

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-01-04 02:49:07 UTC
  • mfrom: (3112.1.5 contrib_bzr_access)
  • Revision ID: pqm@pqm.ubuntu.com-20080104024907-k4ld11wri0m0tvxq
(Balint Aradi) Add contrib/bzr_access to allow some access control
        via a single user based on ssh-keys.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
###############################################################################
 
3
#
 
4
#  bzr_access:
 
5
#    Simple access control for shared bazaar repository accessed over ssh.
 
6
#
 
7
# Copyright (C) 2007 Balint Aradi
 
8
#
 
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.
 
13
#
 
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.
 
18
#
 
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
 
22
#
 
23
###############################################################################
 
24
"""
 
25
Invocation: bzr_access <bzr_executable> <repo_collection> <user>
 
26
 
 
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
 
34
bzr executable.
 
35
 
 
36
Config file: INI format, pretty much similar to the authfile of subversion.
 
37
 
 
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
 
41
commas.)
 
42
 
 
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.
 
48
 
 
49
Only the options in the section with the longest matching name are evaluated.
 
50
The last relevant option for the user is used.
 
51
 
 
52
Sample bzr_access.conf::
 
53
 
 
54
   [groups]
 
55
   admins = alpha
 
56
   devels = beta, gamma, delta
 
57
   
 
58
   [/test/trunk]
 
59
   @admins = rw
 
60
   @devels = r
 
61
   
 
62
   [/test/branches]
 
63
   @admins = rw
 
64
   @devels = rw
 
65
 
 
66
 
 
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::
 
69
 
 
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>
 
71
"""
 
72
 
 
73
import ConfigParser
 
74
import os
 
75
import re
 
76
import subprocess
 
77
import sys
 
78
 
 
79
CONFIG_FILE = "bzr_access.conf"
 
80
SCRIPT_NAME = os.path.basename(sys.argv[0])
 
81
 
 
82
# Permission constants
 
83
PERM_DENIED = 0
 
84
PERM_READ = 1
 
85
PERM_READWRITE = 2
 
86
PERM_DICT = { "r": PERM_READ, "rw": PERM_READWRITE }
 
87
 
 
88
# Exit codes
 
89
EXIT_BAD_NR_ARG = 1
 
90
EXIT_BZR_NOEXEC = 2
 
91
EXIT_REPO_NOREAD = 3
 
92
EXIT_BADENV = 4
 
93
EXIT_BADDIR = 5
 
94
EXIT_NOCONF = 6
 
95
EXIT_NOACCESS = 7
 
96
EXIT_BADUSERNAME = 8
 
97
 
 
98
# pattern for the bzr command passed to ssh
 
99
PAT_SSH_COMMAND = re.compile(r"""^bzr\s+
 
100
                             serve\s+
 
101
                             --inet\s+
 
102
                             --directory=(?P<dir>\S+)\s+
 
103
                             --allow-writes\s*$""", re.VERBOSE)
 
104
 
 
105
# Command line for starting bzr
 
106
BZR_OPTIONS = ['serve', '--inet', '--directory']
 
107
BZR_READWRITE_FLAGS = ['--allow-writes']
 
108
 
 
109
 
 
110
 
 
111
def error(msg, exit_code):
 
112
    """Prints error message to stdout and exits with given error code."""
 
113
    
 
114
    print >>sys.stderr, "%s::error: %s" % (SCRIPT_NAME, msg)
 
115
    sys.exit(exit_code)
 
116
  
 
117
 
 
118
 
 
119
class AccessManager(object):
 
120
    """Manages the permissions, can be queried for a specific user and path."""
 
121
    
 
122
    def __init__(self, fp):
 
123
        """:param fp: File like object, containing the configuration options.
 
124
        """
 
125
        # TODO: jam 20071211 Consider switching to bzrlib.util.configobj
 
126
        self.config = ConfigParser.ConfigParser()
 
127
        self.config.readfp(fp)
 
128
        self.groups = {}
 
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(",")])
 
132
        
 
133
 
 
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.
 
138
        :return: permission.
 
139
        """
 
140
        if not path.startswith("/"):
 
141
            return PERM_DENIED
 
142
        perm = PERM_DENIED
 
143
        pathFound = False
 
144
        while not pathFound and path != "/":
 
145
            print >>sys.stderr, "DEBUG:", path
 
146
            pathFound = self.config.has_section(path)
 
147
            if (pathFound):
 
148
                options = reversed(self.config.options(path))
 
149
                for option in options:
 
150
                    value = PERM_DICT.get(self.config.get(path, option),
 
151
                                          PERM_DENIED)
 
152
                    if self._is_relevant(option, user):
 
153
                        perm = value
 
154
            else:
 
155
                path = os.path.dirname(path)
 
156
        return perm
 
157
      
 
158
      
 
159
    def _is_relevant(self, option, user):
 
160
        """Decides if a certain option is relevant for a given user.
 
161
      
 
162
        An option is relevant if it is identical with the user or with a
 
163
        reference to a group including the user.
 
164
      
 
165
        :param option: Option to check.
 
166
        :param user: User
 
167
        :return: True if option is relevant for the user, False otherwise.
 
168
        """
 
169
        if option.startswith("@"):
 
170
            result = (user in self.groups.get(option[1:], set()))
 
171
        else:
 
172
            result = (option == user)
 
173
        return result
 
174
 
 
175
 
 
176
 
 
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 '/'.
 
182
    """
 
183
    match = PAT_SSH_COMMAND.match(command)
 
184
    if not match:
 
185
        return ""
 
186
    directory = match.group("dir")
 
187
    return os.path.normpath(directory)
 
188
 
 
189
 
 
190
 
 
191
############################################################################
 
192
# Main program
 
193
############################################################################
 
194
def main():
 
195
    # Read arguments
 
196
    if len(sys.argv) != 4:
 
197
        error("Invalid number or arguments.", EXIT_BAD_NR_ARG)
 
198
    (bzrExec, repoRoot, user) = sys.argv[1:4]
 
199
    
 
200
    # Sanity checks
 
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)
 
205
    
 
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)
 
212
 
 
213
    # Control user name
 
214
    if not user.isalnum():
 
215
        error("Invalid user name", EXIT_BADUSERNAME)
 
216
    
 
217
    # Read in config file.
 
218
    try:
 
219
        fp = open(os.path.join(repoRoot, CONFIG_FILE), "r")
 
220
        try:
 
221
            accessMan = AccessManager(fp)
 
222
        finally:
 
223
            fp.close()
 
224
    except IOError:
 
225
        error("Can't read config file.", EXIT_NOCONF)
 
226
    
 
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
 
233
        pass
 
234
    elif perm == PERM_READWRITE:
 
235
        # Add the write flags
 
236
        command.extend(BZR_READWRITE_FLAGS)
 
237
    else:
 
238
        error("Access denied.", EXIT_NOACCESS)
 
239
    return subprocess.call(command)
 
240
 
 
241
 
 
242
if __name__ == "__main__":
 
243
  main()
 
244
 
 
245
 
 
246
### Local Variables:
 
247
### mode:python
 
248
### End: