2 Enforce git-shell to only serve allowed by access control policy.
3 directory. The client should refer to them without any extra directory
4 prefix. Repository names are forced to match ALLOW_RE.
11 from gitosis import access
12 from gitosis import repository
13 from gitosis import gitweb
14 from gitosis import gitdaemon
15 from gitosis import app
16 from gitosis import util
18 log = logging.getLogger('gitosis.serve')
20 ALLOW_RE = re.compile("^'/*(?P<path>[a-zA-Z0-9][a-zA-Z0-9@._-]*(/[a-zA-Z0-9][a-zA-Z0-9@._-]*)*)'$")
30 class ServingError(Exception):
34 return '%s' % self.__doc__
36 class CommandMayNotContainNewlineError(ServingError):
37 """Command may not contain newline"""
39 class UnknownCommandError(ServingError):
40 """Unknown command denied"""
42 class UnsafeArgumentsError(ServingError):
43 """Arguments to command look dangerous"""
45 class AccessDenied(ServingError):
46 """Access denied to repository"""
48 class WriteAccessDenied(AccessDenied):
49 """Repository write access denied"""
51 class ReadAccessDenied(AccessDenied):
52 """Repository read access denied"""
60 raise CommandMayNotContainNewlineError()
63 verb, args = command.split(None, 1)
65 # all known commands take one argument; improve if/when needed
66 raise UnknownCommandError()
68 if (verb not in COMMANDS_WRITE
69 and verb not in COMMANDS_READONLY):
70 raise UnknownCommandError()
72 match = ALLOW_RE.match(args)
74 raise UnsafeArgumentsError()
76 path = match.group('path')
78 # write access is always sufficient
79 newpath = access.haveAccess(
86 # didn't have write access; try once more with the popular
88 newpath = access.haveAccess(
93 if newpath is not None:
95 'Repository %r config has typo "writeable", '
96 +'should be "writable"',
101 # didn't have write access
103 newpath = access.haveAccess(
110 raise ReadAccessDenied()
112 if verb in COMMANDS_WRITE:
113 # didn't have write access and tried to write
114 raise WriteAccessDenied()
116 (topdir, relpath) = newpath
117 assert not relpath.endswith('.git'), \
118 'git extension should have been stripped: %r' % relpath
119 repopath = '%s.git' % relpath
120 fullpath = os.path.join(topdir, repopath)
121 if (not os.path.exists(fullpath)
122 and verb in COMMANDS_WRITE):
123 # it doesn't exist on the filesystem, but the configuration
124 # refers to it, we're serving a write request, and the user is
125 # authorized to do that: create the repository on the fly
127 # create leading directories
129 for segment in repopath.split(os.sep)[:-1]:
130 p = os.path.join(p, segment)
133 repository.init(path=fullpath)
134 gitweb.set_descriptions(
137 generated = util.getGeneratedFilesDir(config=cfg)
138 gitweb.generate_project_list(
140 path=os.path.join(generated, 'projects.list'),
142 gitdaemon.set_export_ok(
146 # put the verb back together with the new path
147 newcmd = "%(verb)s '%(path)s'" % dict(
154 def create_parser(self):
155 parser = super(Main, self).create_parser()
156 parser.set_usage('%prog [OPTS] USER')
157 parser.set_description(
158 'Allow restricted git operations under DIR')
161 def handle_args(self, parser, cfg, options, args):
165 parser.error('Missing argument USER.')
167 main_log = logging.getLogger('gitosis.serve.main')
170 cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
172 main_log.error('Need SSH_ORIGINAL_COMMAND in environment.')
175 main_log.debug('Got command %(cmd)r' % dict(
179 os.chdir(os.path.expanduser('~'))
187 except ServingError, e:
188 main_log.error('%s', e)
191 main_log.debug('Serving %s', newcmd)
192 os.execvp('git-shell', ['git-shell', '-c', newcmd])
193 main_log.error('Cannot execute git-shell.')