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@._-]*)*)'$")
34 class ServingError(Exception):
38 return '%s' % self.__doc__
40 class CommandMayNotContainNewlineError(ServingError):
41 """Command may not contain newline"""
43 class UnknownCommandError(ServingError):
44 """Unknown command denied"""
46 class CommandTooLongError(ServingError):
47 """Command malformed - too many args"""
49 class UnsafeArgumentsError(ServingError):
50 """Arguments to command look dangerous"""
52 class AccessDenied(ServingError):
53 """Access denied to repository"""
55 class WriteAccessDenied(AccessDenied):
56 """Repository write access denied"""
58 class ReadAccessDenied(AccessDenied):
59 """Repository read access denied"""
67 raise CommandMayNotContainNewlineError()
70 verb, args = command.split(None, 1)
72 # all known "git-foo" commands take one argument; improve
74 raise CommandTooLongError()
78 subverb, args = args.split(None, 1)
80 # all known "git foo" commands take one argument; improve
82 raise CommandTooLongError()
83 verb = '%s %s' % (verb, subverb)
85 if (verb not in COMMANDS_WRITE
86 and verb not in COMMANDS_READONLY):
87 raise UnknownCommandError()
89 match = ALLOW_RE.match(args)
91 raise UnsafeArgumentsError()
93 path = match.group('path')
95 # write access is always sufficient
96 newpath = access.haveAccess(
103 # didn't have write access; try once more with the popular
105 newpath = access.haveAccess(
110 if newpath is not None:
112 'Repository %r config has typo "writeable", '
113 +'should be "writable"',
118 # didn't have write access
120 newpath = access.haveAccess(
127 raise ReadAccessDenied()
129 if verb in COMMANDS_WRITE:
130 # didn't have write access and tried to write
131 raise WriteAccessDenied()
133 (topdir, relpath) = newpath
134 assert not relpath.endswith('.git'), \
135 'git extension should have been stripped: %r' % relpath
136 repopath = '%s.git' % relpath
137 fullpath = os.path.join(topdir, repopath)
138 if not os.path.exists(fullpath):
139 # it doesn't exist on the filesystem, but the configuration
140 # refers to it, we're serving a write request, and the user is
141 # authorized to do that: create the repository on the fly
143 # create leading directories
145 for segment in repopath.split(os.sep)[:-1]:
146 p = os.path.join(p, segment)
149 repository.init(path=fullpath)
150 gitweb.set_descriptions(
153 generated = util.getGeneratedFilesDir(config=cfg)
154 gitweb.generate_project_list(
156 path=os.path.join(generated, 'projects.list'),
158 gitdaemon.set_export_ok(
162 # put the verb back together with the new path
163 newcmd = "%(verb)s '%(path)s'" % dict(
170 def create_parser(self):
171 parser = super(Main, self).create_parser()
172 parser.set_usage('%prog [OPTS] USER')
173 parser.set_description(
174 'Allow restricted git operations under DIR')
177 def handle_args(self, parser, cfg, options, args):
181 parser.error('Missing argument USER.')
183 main_log = logging.getLogger('gitosis.serve.main')
186 cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
188 main_log.error('Need SSH_ORIGINAL_COMMAND in environment.')
191 main_log.debug('Got command %(cmd)r' % dict(
195 os.chdir(os.path.expanduser('~'))
203 except ServingError, e:
204 main_log.error('%s', e)
207 main_log.debug('Serving %s', newcmd)
208 os.execvp('git', ['git', 'shell', '-c', newcmd])
209 main_log.error('Cannot execute git-shell.')