1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
"""
Enforce git-shell to only serve allowed by access control policy.
directory. The client should refer to them without any extra directory
prefix. Repository names are forced to match ALLOW_RE.
"""
import logging
import sys, os, optparse, re
from ConfigParser import RawConfigParser
from gitosis import access
def die(msg):
print >>sys.stderr, '%s: %s' % (sys.argv[0], msg)
sys.exit(1)
def getParser():
parser = optparse.OptionParser(
usage='%prog [--config=FILE] USER',
description='Allow restricted git operations under DIR',
)
parser.set_defaults(
config=os.path.expanduser('~/.gitosis.conf'),
)
parser.add_option('--config',
metavar='FILE',
help='read config from FILE',
)
return parser
ALLOW_RE = re.compile("^'(?P<path>[a-zA-Z0-9][a-zA-Z0-9@._-]*(/[a-zA-Z0-9][a-zA-Z0-9@._-]*)*)'$")
COMMANDS_READONLY = [
'git-upload-pack',
]
COMMANDS_WRITE = [
'git-receive-pack',
]
class ServingError(Exception):
"""Serving error"""
def __str__(self):
return '%s' % self.__doc__
class CommandMayNotContainNewlineError(ServingError):
"""Command may not contain newline"""
class UnknownCommandError(ServingError):
"""Unknown command denied"""
class UnsafeArgumentsError(ServingError):
"""Arguments to command look dangerous"""
class AccessDenied(ServingError):
"""Access denied"""
class WriteAccessDenied(AccessDenied):
"""Write access denied"""
class ReadAccessDenied(AccessDenied):
"""Read access denied"""
def serve(
cfg,
user,
command,
):
if '\n' in command:
raise CommandMayNotContainNewlineError()
verb, args = command.split(None, 1)
if (verb not in COMMANDS_WRITE
and verb not in COMMANDS_READONLY):
raise UnknownCommandError()
match = ALLOW_RE.match(args)
if match is None:
raise UnsafeArgumentsError()
path = match.group('path')
# write access is always sufficient
newpath = access.haveAccess(
config=cfg,
user=user,
mode='writable',
path=path)
if newpath is None:
# didn't have write access
newpath = access.haveAccess(
config=cfg,
user=user,
mode='readonly',
path=path)
if newpath is None:
raise ReadAccessDenied()
if verb in COMMANDS_WRITE:
# didn't have write access and tried to write
raise WriteAccessDenied()
# put the verb back together with the new path
newcmd = "%(verb)s '%(newpath)s'" % dict(
verb=verb,
newpath=newpath,
)
return newcmd
def main():
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('gitosis.serve.main')
os.umask(0022)
parser = getParser()
(options, args) = parser.parse_args()
try:
(user,) = args
except ValueError:
parser.error('Missing argument USER.')
cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
if cmd is None:
die("Need SSH_ORIGINAL_COMMAND in environment.")
log.debug('Got command %(cmd)r' % dict(
cmd=cmd,
))
cfg = RawConfigParser()
try:
conffile = file(options.config)
except (IOError, OSError), e:
# I trust the exception has the path.
die("Unable to read config file: %s." % e)
try:
cfg.readfp(conffile)
finally:
conffile.close()
os.chdir(os.path.expanduser('~'))
try:
newcmd = serve(
cfg=cfg,
user=user,
command=cmd,
)
except ServingError, e:
die(str(e))
log.debug('Serving %s', newcmd)
os.execvpe('git-shell', ['git-shell', '-c', newcmd], {})
die("Cannot execute git-shell.")
|