1 | ''' |
---|
2 | BSD Licence |
---|
3 | Copyright (c) 2012, Science & Technology Facilities Council (STFC) |
---|
4 | All rights reserved. |
---|
5 | |
---|
6 | Redistribution and use in source and binary forms, with or without modification, |
---|
7 | are permitted provided that the following conditions are met: |
---|
8 | |
---|
9 | * Redistributions of source code must retain the above copyright notice, |
---|
10 | this list of conditions and the following disclaimer. |
---|
11 | * Redistributions in binary form must reproduce the above copyright notice, |
---|
12 | this list of conditions and the following disclaimer in the documentation |
---|
13 | and/or other materials provided with the distribution. |
---|
14 | * Neither the name of the Science & Technology Facilities Council (STFC) |
---|
15 | nor the names of its contributors may be used to endorse or promote |
---|
16 | products derived from this software without specific prior written permission. |
---|
17 | |
---|
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
---|
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
---|
20 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
---|
21 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
---|
22 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, |
---|
23 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
---|
24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
---|
25 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
---|
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
---|
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
28 | |
---|
29 | Created on 2 Nov 2012 |
---|
30 | |
---|
31 | @author: mnagni |
---|
32 | ''' |
---|
33 | from paste.auth import auth_tkt |
---|
34 | from paste.auth.auth_tkt import BadTicket |
---|
35 | from django.conf import settings |
---|
36 | from dj_security_middleware.exception import DJMiddlewareException,\ |
---|
37 | MissingCookieException |
---|
38 | from django.utils.http import urlencode |
---|
39 | from django.http import HttpResponseRedirect |
---|
40 | import socket |
---|
41 | import logging |
---|
42 | import urlparse |
---|
43 | import re |
---|
44 | import base64 |
---|
45 | from django.contrib.auth.views import logout |
---|
46 | from cookielib import CookieJar |
---|
47 | |
---|
48 | # Get an instance of a logger |
---|
49 | LOGGER = logging.getLogger() |
---|
50 | |
---|
51 | LOGIN_SERVICE_ERROR = 'No LOGIN_SETTING parameter is defined in the \ |
---|
52 | application settings.py file. Please define a proper URL to the \ |
---|
53 | authenticating service' |
---|
54 | |
---|
55 | DJ_SECURITY_SHAREDSECRET_ERROR = 'No DJ_SECURITY_SHAREDSECRET parameter \ |
---|
56 | is defined in the application settings.py file. \ |
---|
57 | Please define it accordingly to the used LOGIN_SERVICE' |
---|
58 | |
---|
59 | AUTHENTICATION_COOKIE_MISSING = 'The expected cookie is missing. \ |
---|
60 | Redirect to the authentication service' |
---|
61 | |
---|
62 | DJ_MIDDLEWARE_IP_ERROR = 'No DJ_MIDDLEWARE_IP parameter \ |
---|
63 | is defined in the application settings.py file. \ |
---|
64 | Please define it accordingly to the machine/proxy seen by the LOGIN_SERVICE' |
---|
65 | |
---|
66 | AUTH_TKT = 'auth_tkt' |
---|
67 | |
---|
68 | class DJ_Security_Middleware(object): |
---|
69 | """ |
---|
70 | Validates if the actual user is authenticated agains a |
---|
71 | given authentication service. |
---|
72 | Actually the middleware intercepts all the requests submitted |
---|
73 | to the underlying Django application and verifies if the presence |
---|
74 | or not of a valid paste cookie in the request. |
---|
75 | """ |
---|
76 | def process_request(self, request): |
---|
77 | url_fiters = getattr(settings, 'DJ_SECURITY_FILTER', None) |
---|
78 | if url_fiters \ |
---|
79 | and security_url_filter(request.path, url_fiters): |
---|
80 | return |
---|
81 | |
---|
82 | if not getattr(settings, 'DJ_SECURITY_LOGIN_SERVICE', None): |
---|
83 | raise DJMiddlewareException(LOGIN_SERVICE_ERROR) |
---|
84 | if not getattr(settings, 'DJ_SECURITY_SHAREDSECRET', None): |
---|
85 | raise DJMiddlewareException(DJ_SECURITY_SHAREDSECRET_ERROR) |
---|
86 | |
---|
87 | custom_auth = getattr(settings, 'DJ_SECURITY_AUTH_CHECK', None) |
---|
88 | if custom_auth: |
---|
89 | try: |
---|
90 | if custom_auth(request): |
---|
91 | return |
---|
92 | #Cannot specify the Exception type as don't know the |
---|
93 | # exceptions type raised by custom_auth |
---|
94 | except Exception: |
---|
95 | pass |
---|
96 | |
---|
97 | #if not settings.DJ_MIDDLEWARE_IP: |
---|
98 | # raise DJMiddlewareException(DJ_MIDDLEWARE_IP_ERROR) |
---|
99 | |
---|
100 | try: |
---|
101 | timestamp, userid, tokens, user_data = _is_authenticated(request) |
---|
102 | request.authenticated_user = {'timestamp': timestamp, \ |
---|
103 | 'userid': userid, \ |
---|
104 | 'tokens': tokens, \ |
---|
105 | 'user_data': user_data} |
---|
106 | LOGGER.debug("stored in request - userid:%s, user_data:%s" % (userid, user_data)) |
---|
107 | pass |
---|
108 | except MissingCookieException as ex: |
---|
109 | LOGGER.info("Missing 'auth_tkt' cookie") |
---|
110 | except DJMiddlewareException as ex: |
---|
111 | LOGGER.info(ex) |
---|
112 | url = '%s?%s' % (settings.DJ_SECURITY_LOGIN_SERVICE, _build_ret_url(request)) |
---|
113 | LOGGER.info("error in authentication. Redirecting to %s" % (url)) |
---|
114 | return HttpResponseRedirect(url) |
---|
115 | |
---|
116 | def process_response(self, request, response): |
---|
117 | if len(request.REQUEST.get('logout', '')) > 0: |
---|
118 | response.delete_cookie(AUTH_TKT) |
---|
119 | return response |
---|
120 | |
---|
121 | def _build_ret_url(request): |
---|
122 | hostname = socket.getfqdn() |
---|
123 | if request.META['SERVER_PORT'] != 80: |
---|
124 | hostname = "%s:%s" % (hostname, request.META['SERVER_PORT']) |
---|
125 | qs = {} |
---|
126 | qs['r'] = base64.b64encode('http://%s%s?%s' % (hostname, request.path, request.GET.urlencode())) |
---|
127 | return urlencode(qs) |
---|
128 | |
---|
129 | def _is_authenticated(request): |
---|
130 | """ |
---|
131 | Verifies the presence and validity of a paste cookie. |
---|
132 | If the cookie is not present the request is redirected |
---|
133 | to the url specified in LOGIN_SERVICE |
---|
134 | ** Return ** a tuple containing (timestamp, userid, tokens, user_data) |
---|
135 | ** raise ** a DJ_SecurityException if the ticket is not valid |
---|
136 | """ |
---|
137 | if AUTH_TKT in request.COOKIES: |
---|
138 | LOGGER.debug("Found auth_tkt: %s in cookies" % (request.COOKIES.get('auth_tkt'))) |
---|
139 | try: |
---|
140 | |
---|
141 | return auth_tkt.parse_ticket( |
---|
142 | settings.DJ_SECURITY_SHAREDSECRET, |
---|
143 | request.COOKIES.get(AUTH_TKT, ''), |
---|
144 | _get_host_ip()) |
---|
145 | except BadTicket as ex: |
---|
146 | raise DJMiddlewareException(ex) |
---|
147 | raise MissingCookieException(AUTHENTICATION_COOKIE_MISSING) |
---|
148 | |
---|
149 | def _calculate_remote_ip(url_path): |
---|
150 | remote_url = urlparse.urlparse(url_path) |
---|
151 | LOGGER.debug("calculating remote_ip for %s" % (str(remote_url))) |
---|
152 | port = 80 |
---|
153 | host = None |
---|
154 | if remote_url.netloc: |
---|
155 | host = remote_url.netloc |
---|
156 | elif remote_url.path: |
---|
157 | host = remote_url.path |
---|
158 | |
---|
159 | if not host: |
---|
160 | return None |
---|
161 | |
---|
162 | if ':' in host: |
---|
163 | host, port = host.split(':') |
---|
164 | addrinfo = socket.getaddrinfo(host, int(port)) |
---|
165 | LOGGER.debug("%s has remote_ip %s" % (url_path, addrinfo[0][-1][0])) |
---|
166 | for remote_url in addrinfo: |
---|
167 | if not remote_url[-1][0].startswith('127'): |
---|
168 | return remote_url[-1][0] |
---|
169 | return None |
---|
170 | |
---|
171 | def _get_host_ip(): |
---|
172 | if getattr(settings, 'DJ_MIDDLEWARE_IP', None): |
---|
173 | return settings.DJ_MIDDLEWARE_IP |
---|
174 | |
---|
175 | return _calculate_remote_ip(socket.getfqdn()) |
---|
176 | |
---|
177 | def security_url_filter(string, filters): |
---|
178 | """ |
---|
179 | Checks a given url request against a list of url filters. |
---|
180 | ** string ** string a url |
---|
181 | ** filters ** a list of strings |
---|
182 | ** RETURN ** True if a match is found, False otherwise |
---|
183 | """ |
---|
184 | try: |
---|
185 | result = urlparse.urlparse(string) |
---|
186 | return _security_filter(result.path, filters) |
---|
187 | except AttributeError: |
---|
188 | return False |
---|
189 | |
---|
190 | def _security_filter(string, filters): |
---|
191 | """ |
---|
192 | Checks a given strings against a list of strings. |
---|
193 | ** string ** string a url |
---|
194 | ** filters ** a list of strings |
---|
195 | """ |
---|
196 | if not filters or not string or len(string.strip()) == 0: |
---|
197 | return False |
---|
198 | if string in filters: |
---|
199 | return True |
---|
200 | for ifilter in filters: |
---|
201 | if re.match(ifilter, string): |
---|
202 | return True |
---|
203 | |
---|
204 | def logout(request): |
---|
205 | """ |
---|
206 | Removes the authenticated user's ID from the request and flushes their |
---|
207 | session data, plus remove the auth_cookie |
---|
208 | """ |
---|
209 | logout(request) |
---|
210 | CookieJar.clear(name = 'auth_tkt') |
---|