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@._-]*)*)'$")
32 class ServingError(Exception):
36 return '%s' % self.__doc__
38 class CommandMayNotContainNewlineError(ServingError):
39 """Command may not contain newline"""
41 class UnknownCommandError(ServingError):
42 """Unknown command denied"""
44 class UnsafeArgumentsError(ServingError):
45 """Arguments to command look dangerous"""
47 class AccessDenied(ServingError):
48 """Access denied to repository"""
50 class WriteAccessDenied(AccessDenied):
51 """Repository write access denied"""
53 class ReadAccessDenied(AccessDenied):
54 """Repository read access denied"""
62 raise CommandMayNotContainNewlineError()
65 verb, args = command.split(None, 1)
67 # all known "git-foo" commands take one argument; improve
69 raise UnknownCommandError()
73 subverb, args = args.split(None, 1)
75 # all known "git foo" commands take one argument; improve
77 raise UnknownCommandError()
78 verb = '%s %s' % (verb, subverb)
80 if (verb not in COMMANDS_WRITE
81 and verb not in COMMANDS_READONLY):
82 raise UnknownCommandError()
84 match = ALLOW_RE.match(args)
86 raise UnsafeArgumentsError()
88 path = match.group('path')
90 # write access is always sufficient
91 newpath = access.haveAccess(
98 # didn't have write access; try once more with the popular
100 newpath = access.haveAccess(
105 if newpath is not None:
107 'Repository %r config has typo "writeable", '
108 +'should be "writable"',
113 # didn't have write access
115 newpath = access.haveAccess(
122 raise ReadAccessDenied()
124 if verb in COMMANDS_WRITE:
125 # didn't have write access and tried to write
126 raise WriteAccessDenied()
128 (topdir, relpath) = newpath
129 assert not relpath.endswith('.git'), \
130 'git extension should have been stripped: %r' % relpath
131 repopath = '%s.git' % relpath
132 fullpath = os.path.join(topdir, repopath)
133 if (not os.path.exists(fullpath)
134 and verb in COMMANDS_WRITE):
135 # it doesn't exist on the filesystem, but the configuration
136 # refers to it, we're serving a write request, and the user is
137 # authorized to do that: create the repository on the fly
139 # create leading directories
141 for segment in repopath.split(os.sep)[:-1]:
142 p = os.path.join(p, segment)
145 repository.init(path=fullpath)
146 gitweb.set_descriptions(
149 generated = util.getGeneratedFilesDir(config=cfg)
150 gitweb.generate_project_list(
152 path=os.path.join(generated, 'projects.list'),
154 gitdaemon.set_export_ok(
158 # put the verb back together with the new path
159 newcmd = "%(verb)s '%(path)s'" % dict(
166 def create_parser(self):
167 parser = super(Main, self).create_parser()
168 parser.set_usage('%prog [OPTS] USER')
169 parser.set_description(
170 'Allow restricted git operations under DIR')
173 def handle_args(self, parser, cfg, options, args):
177 parser.error('Missing argument USER.')
179 main_log = logging.getLogger('gitosis.serve.main')
182 cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
184 main_log.error('Need SSH_ORIGINAL_COMMAND in environment.')
187 main_log.debug('Got command %(cmd)r' % dict(
191 os.chdir(os.path.expanduser('~'))
199 except ServingError, e:
200 main_log.error('%s', e)
203 main_log.debug('Serving %s', newcmd)
204 os.execvp('git', ['git', 'shell', '-c', newcmd])
205 main_log.error('Cannot execute git-shell.')