~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to contrib/bzr_access

Merge the 0.17 fixes back into bzr.dev

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: