~bzr-pqm/bzr/bzr.dev

3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
1
#!/usr/bin/env python
2
###############################################################################
3
#
4
#  bzr_access:
5
#    Simple access control for shared bazaar repository accessed over ssh.
6
#
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
7
# Copyright (C) 2007 Balint Aradi
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
8
#
3099.4.6 by John Arbash Meinel
Add GPL license to bzr_access script
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
#
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
23
###############################################################################
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
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
3112.1.4 by Balint Aradi
Changing example to show comma separated user names.
41
commas.)
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
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
3112.1.4 by Balint Aradi
Changing example to show comma separated user names.
56
   devels = beta, gamma, delta
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
57
   
58
   [/test/trunk]
59
   @admins = rw
60
   @devels = r
61
   
62
   [/test/branches]
63
   @admins = rw
64
   @devels = rw
65
66
3099.4.5 by John Arbash Meinel
A bit more documentation cleanup
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::
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
69
3099.4.5 by John Arbash Meinel
A bit more documentation cleanup
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>
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
71
"""
72
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
73
import ConfigParser
3099.4.2 by John Arbash Meinel
Some simple spelling fixes, and switch to using subprocess since that is space-in-filename safe
74
import os
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
75
import re
3099.4.2 by John Arbash Meinel
Some simple spelling fixes, and switch to using subprocess since that is space-in-filename safe
76
import subprocess
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
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
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
96
EXIT_BADUSERNAME = 8
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
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
3099.4.4 by John Arbash Meinel
Change initial comment into the docstring, and update snippets to be proper ReST
105
# Command line for starting bzr
3099.4.2 by John Arbash Meinel
Some simple spelling fixes, and switch to using subprocess since that is space-in-filename safe
106
BZR_OPTIONS = ['serve', '--inet', '--directory']
107
BZR_READWRITE_FLAGS = ['--allow-writes']
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
108
109
110
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
111
def error(msg, exit_code):
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
112
    """Prints error message to stdout and exits with given error code."""
113
    
114
    print >>sys.stderr, "%s::error: %s" % (SCRIPT_NAME, msg)
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
115
    sys.exit(exit_code)
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
116
  
117
118
119
class AccessManager(object):
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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
        """
3099.4.7 by John Arbash Meinel
Add a comment about using configobj instead of ConfigParser
125
        # TODO: jam 20071211 Consider switching to bzrlib.util.configobj
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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"):
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
131
                self.groups[group] = set([ s.strip() for s in users.split(",")])
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
132
        
133
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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 != "/":
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
145
            print >>sys.stderr, "DEBUG:", path
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
146
            pathFound = self.config.has_section(path)
147
            if (pathFound):
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
148
                options = reversed(self.config.options(path))
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
149
                for option in options:
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
150
                    value = PERM_DICT.get(self.config.get(path, option),
151
                                          PERM_DENIED)
152
                    if self._is_relevant(option, user):
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
153
                        perm = value
154
            else:
155
                path = os.path.dirname(path)
156
        return perm
157
      
158
      
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
159
    def _is_relevant(self, option, user):
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
160
        """Decides if a certain option is relevant for a given user.
161
      
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
162
        An option is relevant if it is identical with the user or with a
163
        reference to a group including the user.
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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("@"):
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
170
            result = (user in self.groups.get(option[1:], set()))
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
171
        else:
172
            result = (option == user)
173
        return result
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
174
175
176
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
177
def get_directory(command):
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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)
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
188
189
190
191
############################################################################
192
# Main program
193
############################################################################
194
def main():
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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)
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
209
    directory = get_directory(os.environ["SSH_ORIGINAL_COMMAND"])
3112.1.5 by John Arbash Meinel
a small bit of cleanup.
210
    if len(directory) == 0:
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
211
        error("Bad directory name.", EXIT_BADDIR)
3112.1.3 by Balint Aradi
Tiny changes for more readable code and better compliance with bzr style.
212
213
    # Control user name
214
    if not user.isalnum():
215
        error("Invalid user name", EXIT_BADUSERNAME)
3099.4.3 by John Arbash Meinel
Change the indentation to 4 spaces according to Bazaar style guidelines.
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)
3099.4.1 by Bálint Aradi
Add a bzr_access script for allowing custom access control over bzr+ssh
240
241
242
if __name__ == "__main__":
243
  main()
244
245
246
### Local Variables:
247
### mode:python
248
### End: