~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to contrib/bzr_access

Add a NEWS entry and prepare submission.

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
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.
 
48
 
 
49
Sample bzr_access.conf::
 
50
 
 
51
   [groups]
 
52
   admins = alpha
 
53
   devels = beta, gamma, delta
 
54
   
 
55
   [/]
 
56
   @admins = rw
 
57
   @devels = r
 
58
 
 
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::
 
61
 
 
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>
 
63
"""
 
64
 
 
65
import ConfigParser
 
66
import os
 
67
import re
 
68
import subprocess
 
69
import sys
 
70
 
 
71
CONFIG_FILE = "bzr_access.conf"
 
72
SCRIPT_NAME = os.path.basename(sys.argv[0])
 
73
 
 
74
# Permission constants
 
75
PERM_DENIED = 0
 
76
PERM_READ = 1
 
77
PERM_READWRITE = 2
 
78
PERM_DICT = { "r": PERM_READ, "rw": PERM_READWRITE }
 
79
 
 
80
# Exit codes
 
81
EXIT_BAD_NR_ARG = 1
 
82
EXIT_BZR_NOEXEC = 2
 
83
EXIT_REPO_NOREAD = 3
 
84
EXIT_BADENV = 4
 
85
EXIT_BADDIR = 5
 
86
EXIT_NOCONF = 6
 
87
EXIT_NOACCESS = 7
 
88
EXIT_BADUSERNAME = 8
 
89
 
 
90
# pattern for the bzr command passed to ssh
 
91
PAT_SSH_COMMAND = re.compile(r"""^bzr\s+
 
92
                             serve\s+
 
93
                             --inet\s+
 
94
                             --directory=(?P<dir>\S+)\s+
 
95
                             --allow-writes\s*$""", re.VERBOSE)
 
96
 
 
97
# Command line for starting bzr
 
98
BZR_OPTIONS = ['serve', '--inet', '--directory']
 
99
BZR_READWRITE_FLAGS = ['--allow-writes']
 
100
 
 
101
 
 
102
 
 
103
def error(msg, exit_code):
 
104
    """Prints error message to stdout and exits with given error code."""
 
105
    
 
106
    print >>sys.stderr, "%s::error: %s" % (SCRIPT_NAME, msg)
 
107
    sys.exit(exit_code)
 
108
  
 
109
 
 
110
 
 
111
class AccessManager(object):
 
112
    """Manages the permissions, can be queried for a specific user and path."""
 
113
    
 
114
    def __init__(self, fp):
 
115
        """:param fp: File like object, containing the configuration options.
 
116
        """
 
117
        # TODO: jam 20071211 Consider switching to bzrlib.util.configobj
 
118
        self.config = ConfigParser.ConfigParser()
 
119
        self.config.readfp(fp)
 
120
        self.groups = {}
 
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(",")])
 
124
        
 
125
 
 
126
    def permission(self, user):
 
127
        """Determines the permission for a given user and a given path
 
128
        :param user: user to look for.
 
129
        :return: permission.
 
130
        """
 
131
        configSection = "/"
 
132
        perm = PERM_DENIED
 
133
        pathFound = self.config.has_section(configSection)
 
134
        if (pathFound):
 
135
            options = reversed(self.config.options(configSection))
 
136
            for option in options:
 
137
                value = PERM_DICT.get(self.config.get(configSection, option),
 
138
                                      PERM_DENIED)
 
139
                if self._is_relevant(option, user):
 
140
                    perm = value
 
141
        return perm
 
142
 
 
143
      
 
144
    def _is_relevant(self, option, user):
 
145
        """Decides if a certain option is relevant for a given user.
 
146
      
 
147
        An option is relevant if it is identical with the user or with a
 
148
        reference to a group including the user.
 
149
      
 
150
        :param option: Option to check.
 
151
        :param user: User
 
152
        :return: True if option is relevant for the user, False otherwise.
 
153
        """
 
154
        if option.startswith("@"):
 
155
            result = (user in self.groups.get(option[1:], set()))
 
156
        else:
 
157
            result = (option == user)
 
158
        return result
 
159
 
 
160
 
 
161
 
 
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 '/'.
 
167
    """
 
168
    match = PAT_SSH_COMMAND.match(command)
 
169
    if not match:
 
170
        return ""
 
171
    directory = match.group("dir")
 
172
    return os.path.normpath(directory)
 
173
 
 
174
 
 
175
 
 
176
############################################################################
 
177
# Main program
 
178
############################################################################
 
179
def main():
 
180
    # Read arguments
 
181
    if len(sys.argv) != 4:
 
182
        error("Invalid number or arguments.", EXIT_BAD_NR_ARG)
 
183
    (bzrExec, repoRoot, user) = sys.argv[1:4]
 
184
    
 
185
    # Sanity checks
 
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)
 
190
    
 
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)
 
197
 
 
198
    # Control user name
 
199
    if not user.isalnum():
 
200
        error("Invalid user name", EXIT_BADUSERNAME)
 
201
    
 
202
    # Read in config file.
 
203
    try:
 
204
        fp = open(os.path.join(repoRoot, CONFIG_FILE), "r")
 
205
        try:
 
206
            accessMan = AccessManager(fp)
 
207
        finally:
 
208
            fp.close()
 
209
    except IOError:
 
210
        error("Can't read config file.", EXIT_NOCONF)
 
211
    
 
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
 
217
        pass
 
218
    elif perm == PERM_READWRITE:
 
219
        # Add the write flags
 
220
        command.extend(BZR_READWRITE_FLAGS)
 
221
    else:
 
222
        error("Access denied.", EXIT_NOACCESS)
 
223
    return subprocess.call(command)
 
224
 
 
225
 
 
226
if __name__ == "__main__":
 
227
  main()
 
228
 
 
229
 
 
230
### Local Variables:
 
231
### mode:python
 
232
### End: