Let even readonly operations init a repo if needed.
[gitosis.git] / gitosis / serve.py
blobd83b1d8c5ebd1adb7179a73d083e5043932a379d
1 """
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.
5 """
7 import logging
9 import sys, os, 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@._-]*)*)'$")
22 COMMANDS_READONLY = [
23 'git-upload-pack',
24 'git upload-pack',
27 COMMANDS_WRITE = [
28 'git-receive-pack',
29 'git receive-pack',
32 class ServingError(Exception):
33 """Serving error"""
35 def __str__(self):
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"""
56 def serve(
57 cfg,
58 user,
59 command,
61 if '\n' in command:
62 raise CommandMayNotContainNewlineError()
64 try:
65 verb, args = command.split(None, 1)
66 except ValueError:
67 # all known "git-foo" commands take one argument; improve
68 # if/when needed
69 raise UnknownCommandError()
71 if verb == 'git':
72 try:
73 subverb, args = args.split(None, 1)
74 except ValueError:
75 # all known "git foo" commands take one argument; improve
76 # if/when needed
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)
85 if match is None:
86 raise UnsafeArgumentsError()
88 path = match.group('path')
90 # write access is always sufficient
91 newpath = access.haveAccess(
92 config=cfg,
93 user=user,
94 mode='writable',
95 path=path)
97 if newpath is None:
98 # didn't have write access; try once more with the popular
99 # misspelling
100 newpath = access.haveAccess(
101 config=cfg,
102 user=user,
103 mode='writeable',
104 path=path)
105 if newpath is not None:
106 log.warning(
107 'Repository %r config has typo "writeable", '
108 +'should be "writable"',
109 path,
112 if newpath is None:
113 # didn't have write access
115 newpath = access.haveAccess(
116 config=cfg,
117 user=user,
118 mode='readonly',
119 path=path)
121 if newpath is None:
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 # it doesn't exist on the filesystem, but the configuration
135 # refers to it, we're serving a write request, and the user is
136 # authorized to do that: create the repository on the fly
138 # create leading directories
139 p = topdir
140 for segment in repopath.split(os.sep)[:-1]:
141 p = os.path.join(p, segment)
142 util.mkdir(p, 0750)
144 repository.init(path=fullpath)
145 gitweb.set_descriptions(
146 config=cfg,
148 generated = util.getGeneratedFilesDir(config=cfg)
149 gitweb.generate_project_list(
150 config=cfg,
151 path=os.path.join(generated, 'projects.list'),
153 gitdaemon.set_export_ok(
154 config=cfg,
157 # put the verb back together with the new path
158 newcmd = "%(verb)s '%(path)s'" % dict(
159 verb=verb,
160 path=fullpath,
162 return newcmd
164 class Main(app.App):
165 def create_parser(self):
166 parser = super(Main, self).create_parser()
167 parser.set_usage('%prog [OPTS] USER')
168 parser.set_description(
169 'Allow restricted git operations under DIR')
170 return parser
172 def handle_args(self, parser, cfg, options, args):
173 try:
174 (user,) = args
175 except ValueError:
176 parser.error('Missing argument USER.')
178 main_log = logging.getLogger('gitosis.serve.main')
179 os.umask(0022)
181 cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
182 if cmd is None:
183 main_log.error('Need SSH_ORIGINAL_COMMAND in environment.')
184 sys.exit(1)
186 main_log.debug('Got command %(cmd)r' % dict(
187 cmd=cmd,
190 os.chdir(os.path.expanduser('~'))
192 try:
193 newcmd = serve(
194 cfg=cfg,
195 user=user,
196 command=cmd,
198 except ServingError, e:
199 main_log.error('%s', e)
200 sys.exit(1)
202 main_log.debug('Serving %s', newcmd)
203 os.execvp('git', ['git', 'shell', '-c', newcmd])
204 main_log.error('Cannot execute git-shell.')
205 sys.exit(1)