forked from deshaw/wsgi-kerberos
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwsgi_kerberos.py
executable file
·213 lines (184 loc) · 8.37 KB
/
wsgi_kerberos.py
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
'''
WSGI Kerberos Authentication Middleware
Add Kerberos/GSSAPI Negotiate Authentication support to any WSGI Application
'''
import errno
import kerberos
import logging
import os
import socket
import sys
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.NullHandler())
PY3 = sys.version_info > (3,)
if PY3:
basestring = (bytes, str)
unicode = str
def ensure_bytestring(s):
return s.encode('utf-8') if isinstance(s, unicode) else s
def _consume_request(environ):
'''
Consume and discard all of the data on the request.
This avoids problems that some clients have when they get an unexpected
and premature close from the server.
RFC2616: If an origin server receives a request that does not include an
Expect request-header field with the "100-continue" expectation, the
request includes a request body, and the server responds with a final
status code before reading the entire request body from the transport
connection, then the server SHOULD NOT CLOSE the transport connection until
it has read the entire request, or until the client closes the connection.
Otherwise, the client might not reliably receive the response message.
However, this requirement is not be construed as preventing a server from
defending itself against denial-of-service attacks, or from badly broken
client implementations.
'''
try:
sock = environ.get('wsgi.input')
if hasattr(sock, 'closed') and sock.closed:
return
# Figure out how much content is available for us to consume.
expected = int(environ.get('CONTENT_LENGTH', '0'))
# Try to receive all of the data. Keep retrying until we get an error
# which indicates that we can't retry. Eat errors. The client will just
# have to deal with a possible Broken Pipe -- we tried.
received = 0
while received < expected:
try:
received += len(sock.read(expected - received))
except socket.error as err:
if err.errno != errno.EAGAIN:
break
except (KeyError, ValueError):
pass
class KerberosAuthMiddleware(object):
'''
WSGI Middleware providing Kerberos Authentication
If no hostname is provided, the name returned by socket.gethostname() will
be used.
:param app: WSGI Application
:param hostname: Hostname for kerberos name canonicalization
:type hostname: str
:param unauthorized: 401 Response text or text/content-type tuple
:type unauthorized: str or tuple
:param forbidden: 403 Response text or text/content-type tuple
:type forbidden: str or tuple
:param auth_required_callback: predicate accepting the WSGI environ
for a request returning whether the request should be authenticated
:type auth_required_callback: callable
'''
def __init__(self, app, hostname=None, unauthorized=None, forbidden=None,
auth_required_callback=None):
if hostname is None:
hostname = socket.gethostname()
if unauthorized is None:
unauthorized = (b'Unauthorized', 'text/plain')
elif isinstance(unauthorized, basestring):
unauthorized = (unauthorized, 'text/plain')
unauthorized = (ensure_bytestring(unauthorized[0]), unauthorized[1])
if forbidden is None:
forbidden = (b'Forbidden', 'text/plain')
elif isinstance(forbidden, basestring):
forbidden = (forbidden, 'text/plain')
forbidden = (ensure_bytestring(forbidden[0]), forbidden[1])
if auth_required_callback is None:
auth_required_callback = lambda x: True
self.application = app # WSGI Application
self.service = 'HTTP@%s' % hostname # GSS Service
self.unauthorized = unauthorized # 401 response text/content-type
self.forbidden = forbidden # 403 response text/content-type
self.auth_required_callback = auth_required_callback
if 'KRB5_KTNAME' in os.environ:
try:
principal = kerberos.getServerPrincipalDetails('HTTP',
hostname)
except kerberos.KrbError as exc:
LOG.warning('KerberosAuthMiddleware: %s', exc)
else:
LOG.debug('KerberosAuthMiddleware is identifying as %s', principal)
else:
LOG.warning('KerberosAuthMiddleware: set KRB5_KTNAME to your keytab file')
def _unauthorized(self, environ, start_response, token=None):
'''
Send a 401 Unauthorized response
'''
headers = [('content-type', self.unauthorized[1])]
if token:
headers.append(('WWW-Authenticate', token))
else:
headers.append(('WWW-Authenticate', 'Negotiate'))
_consume_request(environ)
start_response('401 Unauthorized', headers)
return [self.unauthorized[0]]
def _forbidden(self, environ, start_response):
'''
Send a 403 Forbidden response
'''
headers = [('content-type', self.forbidden[1])]
_consume_request(environ)
start_response('403 Forbidden', headers)
return [self.forbidden[0]]
def _authenticate(self, client_token):
'''
Validate the client token
Return the authenticated users principal and a token suitable to
provide mutual authentication to the client.
'''
state = None
server_token = None
user = None
try:
rc, state = kerberos.authGSSServerInit(self.service)
if rc == kerberos.AUTH_GSS_COMPLETE:
rc = kerberos.authGSSServerStep(state, client_token)
if rc == kerberos.AUTH_GSS_COMPLETE:
server_token = kerberos.authGSSServerResponse(state)
user = kerberos.authGSSServerUserName(state)
elif rc == kerberos.AUTH_GSS_CONTINUE:
server_token = kerberos.authGSSServerResponse(state)
except kerberos.GSSError as exc:
LOG.error("Unhandled GSSError: %s", exc)
finally:
if state:
kerberos.authGSSServerClean(state)
return server_token, user
def __call__(self, environ, start_response):
'''
Authenticate the client, and on success invoke the WSGI application.
Include a token in the response headers that can be used to
authenticate the server to the client.
'''
# If we don't need to authenticate the request, shortcut the whole
# process.
if not self.auth_required_callback(environ):
return self.application(environ, start_response)
authorization = environ.get('HTTP_AUTHORIZATION')
# If we have no 'Authorization' header, return a 401.
if authorization is None:
return self._unauthorized(environ, start_response)
# If we have an 'Authorization' header, extract the client's token and
# attempt to authenticate with it.
client_token = ''.join(authorization.split()[1:])
server_token, user = self._authenticate(client_token)
# If we get a server_token and a user, call the application, add our
# token, and return the response for mutual authentication
if server_token and user:
# Add the user to the environment for the application to use it,
# call the application, add the token to the response, and return
# it
environ['REMOTE_USER'] = user
def custom_start_response(status, headers, exc_info=None):
headers.append(('WWW-Authenticate', ' '.join(['negotiate',
server_token])))
return start_response(status, headers, exc_info)
return self.application(environ, custom_start_response)
# If we get a a user, but no token, call the application but don't
# provide mutual authentication.
elif user:
environ['REMOTE_USER'] = user
return self.application(environ, start_response)
elif server_token:
# If we got a token, but no user, return a 401 with the token
return self._unauthorized(environ, start_response, server_token)
else:
# Otherwise, return a 403.
return self._forbidden(environ, start_response)