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 | |
---|
37 | from django.utils.http import urlencode |
---|
38 | from django.http import HttpResponseRedirect |
---|
39 | |
---|
40 | from dj_security_middleware.exception import DJMiddlewareException,\ |
---|
41 | MissingCookieException |
---|
42 | from dj_security_middleware import _get_host_ip, security_filter, \ |
---|
43 | auth_tkt_name, shared_secret, token_field_name, redirect_field_name, \ |
---|
44 | cookie_domain, login_service |
---|
45 | |
---|
46 | import socket |
---|
47 | import logging |
---|
48 | import re |
---|
49 | import urllib |
---|
50 | |
---|
51 | DJ_SECURITY_SHAREDSECRET_ERROR = 'No SECURITY_SHAREDSECRET parameter \ |
---|
52 | is defined in the application settings.py file. \ |
---|
53 | Please define it accordingly to the used LOGIN_SERVICE' |
---|
54 | |
---|
55 | AUTHENTICATION_COOKIE_MISSING = 'The expected cookie is missing. \ |
---|
56 | Redirect to the authentication service' |
---|
57 | |
---|
58 | DJ_MIDDLEWARE_IP_ERROR = 'No DJ_MIDDLEWARE_IP parameter \ |
---|
59 | is defined in the application settings.py file. \ |
---|
60 | Please define it accordingly to the machine/proxy seen by the LOGIN_SERVICE' |
---|
61 | |
---|
62 | LOGOUT = 'logout' |
---|
63 | LOGIN = 'login' |
---|
64 | |
---|
65 | LOGGER = logging.getLogger(__name__) |
---|
66 | |
---|
67 | def preapare_user_for_session(request, timestamp, userid, tokens, user_data): |
---|
68 | request.authenticated_user = {'timestamp': timestamp, \ |
---|
69 | 'userid': userid, \ |
---|
70 | 'tokens': tokens, \ |
---|
71 | 'user_data': user_data} |
---|
72 | LOGGER.debug("stored in request - userid:%s, user_data:%s" % (userid, user_data)) |
---|
73 | request.session['accountid'] = userid |
---|
74 | |
---|
75 | def filter_url(string, filters): |
---|
76 | """ |
---|
77 | Checks a given strings against a list of strings. |
---|
78 | ** string ** string a url |
---|
79 | ** filters ** a list of strings |
---|
80 | """ |
---|
81 | for ifilter in filters: |
---|
82 | if re.search(ifilter, string): |
---|
83 | return True |
---|
84 | |
---|
85 | def is_public_url(request): |
---|
86 | url_fiters = getattr(settings, 'DJ_SECURITY_FILTER', []) |
---|
87 | |
---|
88 | #adds a default filter for reset password request |
---|
89 | reset_regexpr = '%s=[a-f0-9-]*$' % (token_field_name()) |
---|
90 | if reset_regexpr not in security_filter(): |
---|
91 | url_fiters.append(reset_regexpr) |
---|
92 | |
---|
93 | if url_fiters \ |
---|
94 | and filter_url(_build_url(request), url_fiters): |
---|
95 | return True |
---|
96 | return False |
---|
97 | |
---|
98 | class DJ_Security_Middleware(object): |
---|
99 | """ |
---|
100 | Validates if the actual user is authenticated agains a |
---|
101 | given authentication service. |
---|
102 | Actually the middleware intercepts all the requests submitted |
---|
103 | to the underlying Django application and verifies if the presence |
---|
104 | or not of a valid paste cookie in the request. |
---|
105 | """ |
---|
106 | |
---|
107 | def process_request(self, request): |
---|
108 | login_service() |
---|
109 | |
---|
110 | #Has to process a reset password request? |
---|
111 | if len(request.REQUEST.get(LOGOUT, '')) > 0: |
---|
112 | response = HttpResponseRedirect(_build_url(request)) |
---|
113 | response.delete_cookie(auth_tkt_name(), domain = cookie_domain()) |
---|
114 | request.session['accountid'] = None |
---|
115 | return response |
---|
116 | |
---|
117 | custom_auth = getattr(settings, 'DJ_SECURITY_AUTH_CHECK', None) |
---|
118 | if custom_auth: |
---|
119 | try: |
---|
120 | if custom_auth(request): |
---|
121 | return |
---|
122 | #Cannot specify the Exception type as don't know the |
---|
123 | # exceptions type raised by custom_auth |
---|
124 | except Exception: |
---|
125 | pass |
---|
126 | |
---|
127 | #if not settings.DJ_MIDDLEWARE_IP: |
---|
128 | # raise DJMiddlewareException(DJ_MIDDLEWARE_IP_ERROR) |
---|
129 | |
---|
130 | try: |
---|
131 | qs = {redirect_field_name(): |
---|
132 | urllib.quote_plus((_build_url(request)))} |
---|
133 | url = '%s?%s' % (login_service(), |
---|
134 | urlencode(qs)) |
---|
135 | timestamp, userid, tokens, user_data = _is_authenticated(request) |
---|
136 | preapare_user_for_session(request, |
---|
137 | timestamp, |
---|
138 | userid, |
---|
139 | tokens, |
---|
140 | user_data) |
---|
141 | log_msg = '' |
---|
142 | except MissingCookieException: |
---|
143 | log_msg = "Missing cookie '%s'. Redirecting to %s" % (auth_tkt_name(), url) |
---|
144 | except DJMiddlewareException: |
---|
145 | log_msg = "Error in authentication. Redirecting to %s" % (url) |
---|
146 | finally: |
---|
147 | if (len(log_msg) == 0 or is_public_url(request)) \ |
---|
148 | and request.GET.get(LOGIN, None) == None: |
---|
149 | return |
---|
150 | elif len(log_msg) > 0: |
---|
151 | LOGGER.info(log_msg) |
---|
152 | return HttpResponseRedirect(url) |
---|
153 | |
---|
154 | |
---|
155 | |
---|
156 | def process_response(self, request, response): |
---|
157 | return response |
---|
158 | |
---|
159 | def _build_url(request): |
---|
160 | hostname = request.environ.get('HOSTNAME', socket.getfqdn()) |
---|
161 | #hostname = socket.getfqdn() |
---|
162 | new_get = request.GET.copy() |
---|
163 | |
---|
164 | #Removed the LOGIN request attribute as we now know we need to do a login |
---|
165 | new_get.pop(LOGIN, None) |
---|
166 | #Removed the LOGOUT request attribute as we now know we need to do a logout |
---|
167 | new_get.pop(LOGOUT, None) |
---|
168 | |
---|
169 | if request.META['SERVER_PORT'] != 80: |
---|
170 | hostname = "%s:%s" % (hostname, request.META['SERVER_PORT']) |
---|
171 | return 'http://%s%s?%s' % (hostname, |
---|
172 | request.path, |
---|
173 | urllib.urlencode(new_get)) |
---|
174 | |
---|
175 | def _is_authenticated(request): |
---|
176 | """ |
---|
177 | Verifies the presence and validity of a paste cookie. |
---|
178 | If the cookie is not present the request is redirected |
---|
179 | to the url specified in LOGIN_SERVICE |
---|
180 | ** Return ** a tuple containing (timestamp, userid, tokens, user_data) |
---|
181 | ** raise ** a DJ_SecurityException if the ticket is not valid |
---|
182 | """ |
---|
183 | if auth_tkt_name() in request.COOKIES: |
---|
184 | LOGGER.debug("Found cookie '%s': %s in cookies" \ |
---|
185 | % (auth_tkt_name(), request.COOKIES.get(auth_tkt_name()))) |
---|
186 | try: |
---|
187 | |
---|
188 | return auth_tkt.parse_ticket( |
---|
189 | shared_secret(), |
---|
190 | request.COOKIES.get(auth_tkt_name(), ''), |
---|
191 | _get_host_ip()) |
---|
192 | except BadTicket as ex: |
---|
193 | raise DJMiddlewareException(ex) |
---|
194 | raise MissingCookieException(AUTHENTICATION_COOKIE_MISSING) |
---|