1
|
import libxml2
|
2
|
import urllib
|
3
|
import urlparse
|
4
|
import httplib
|
5
|
import re
|
6
|
import os
|
7
|
import socket
|
8
|
import base64
|
9
|
|
10
|
from quixote import get_request, get_response, get_session, redirect, get_publisher
|
11
|
from quixote.directory import Directory
|
12
|
from quixote.http_request import parse_header
|
13
|
|
14
|
import lasso
|
15
|
|
16
|
from qommon import get_logger
|
17
|
from qommon.form import *
|
18
|
from qommon.errors import ConnectionError, ConfigurationError, LoginError
|
19
|
from qommon.misc import http_post_request, http_get_page
|
20
|
from qommon.template import *
|
21
|
|
22
|
from larpe.plugins import site_authentication_plugins
|
23
|
|
24
|
import misc
|
25
|
from users import User
|
26
|
from federations import Federation
|
27
|
|
28
|
class SiteAuthentication:
|
29
|
|
30
|
output_filters = []
|
31
|
|
32
|
def __init__(self, host):
|
33
|
self.host = host
|
34
|
|
35
|
def federate(self, username, password, provider_id, cookies, select):
|
36
|
user = get_session().get_user(provider_id)
|
37
|
if user is not None:
|
38
|
Federation(username, password, self.host.id, user.name_identifiers[0], cookies, select).store()
|
39
|
|
40
|
def sso_local_login(self, federation):
|
41
|
status, data = self.local_auth_check_dispatch(
|
42
|
federation.username, federation.password, federation.select_fields)
|
43
|
success, return_content = self.check_auth(status, data)
|
44
|
if success:
|
45
|
session = get_session()
|
46
|
if hasattr(session, 'cookies'):
|
47
|
federation.set_cookies(session.cookies)
|
48
|
federation.store()
|
49
|
return return_content
|
50
|
else:
|
51
|
return redirect('local_auth')
|
52
|
|
53
|
def local_auth [html] (self, first_time=True):
|
54
|
response = get_response()
|
55
|
response.set_content_type('text/html')
|
56
|
|
57
|
if hasattr(get_response(), str('breadcrumb')):
|
58
|
del get_response().breadcrumb
|
59
|
|
60
|
get_response().filter['default_org'] = '%s - %s' % (self.host.label, _('Local authentication'))
|
61
|
get_response().filter['body_class'] = 'login'
|
62
|
|
63
|
form = self.form_local_auth()
|
64
|
form.add_submit('submit', _('Submit'))
|
65
|
#form.add_submit('cancel', _('Cancel'))
|
66
|
|
67
|
# if form.get_widget('cancel').parse():
|
68
|
# return redirect('.')
|
69
|
authentication_failure = None
|
70
|
if form.is_submitted() and not form.has_errors():
|
71
|
try:
|
72
|
return self.submit_local_auth_form(form)
|
73
|
except LoginError:
|
74
|
authentication_failure = _('Authentication failure')
|
75
|
get_logger().info('local auth page : %s' % authentication_failure)
|
76
|
except ConnectionError, err:
|
77
|
authentication_failure = _('Connection failed : %s') % err
|
78
|
get_logger().info('local auth page : %s' % authentication_failure)
|
79
|
except ConfigurationError, err:
|
80
|
authentication_failure = _('This service provider is not fully configured : %s') % err
|
81
|
get_logger().info('local auth page : %s' % authentication_failure)
|
82
|
except Exception, err:
|
83
|
authentication_failure = _('Unknown error : %s' % err)
|
84
|
get_logger().info('local auth page : %s' % authentication_failure)
|
85
|
|
86
|
if authentication_failure:
|
87
|
'<div class="errornotice">%s</div>' % authentication_failure
|
88
|
'<p>'
|
89
|
_('Please type your login and password for this Service Provider.')
|
90
|
_('Your local account will be federated with your Liberty Alliance account.')
|
91
|
'</p>'
|
92
|
|
93
|
form.render()
|
94
|
|
95
|
# Also used in admin/hosts.ptl
|
96
|
def form_local_auth(self):
|
97
|
form = Form(enctype='multipart/form-data')
|
98
|
form.add(StringWidget, 'username', title = _('Username'), required = True,
|
99
|
size = 30)
|
100
|
form.add(PasswordWidget, 'password', title = _('Password'), required = True,
|
101
|
size = 30)
|
102
|
for name, values in self.host.select_fields.iteritems():
|
103
|
options = []
|
104
|
if values:
|
105
|
for value in values:
|
106
|
options.append(value)
|
107
|
form.add(SingleSelectWidget, name, title = name.capitalize(),
|
108
|
value = values[0], options = options)
|
109
|
return form
|
110
|
|
111
|
def submit_local_auth_form(self, form):
|
112
|
username = form.get_widget('username').parse()
|
113
|
password = form.get_widget('password').parse()
|
114
|
select = {}
|
115
|
for name, values in self.host.select_fields.iteritems():
|
116
|
if form.get_widget(name):
|
117
|
select[name] = form.get_widget(name).parse()
|
118
|
return self.local_auth_check(username, password, select)
|
119
|
|
120
|
def local_auth_check(self, username, password, select=None):
|
121
|
select = select or {}
|
122
|
status, data = self.local_auth_check_dispatch(username, password, select)
|
123
|
if status == 0:
|
124
|
raise
|
125
|
success, return_content = self.check_auth(status, data)
|
126
|
if success:
|
127
|
if misc.get_current_protocol() == lasso.PROTOCOL_SAML_2_0:
|
128
|
provider_id = self.host.saml2_provider_id
|
129
|
else:
|
130
|
provider_id = self.host.provider_id
|
131
|
session = get_session()
|
132
|
|
133
|
if hasattr(session, 'cookies'):
|
134
|
self.federate(username, password, provider_id, session.cookies, select)
|
135
|
else:
|
136
|
self.federate(username, password, provider_id, None, select)
|
137
|
return return_content
|
138
|
raise LoginError()
|
139
|
|
140
|
def local_auth_check_dispatch(self, username, password, select=None):
|
141
|
select = select or {}
|
142
|
if self.host.auth_mode == 'http_basic':
|
143
|
return self.local_auth_check_http_basic(username, password)
|
144
|
elif self.host.auth_mode == 'form' and hasattr(self.host, 'auth_check_url') \
|
145
|
and self.host.auth_check_url is not None:
|
146
|
return self.local_auth_check_post(username, password, select)
|
147
|
else:
|
148
|
raise ConfigurationError('No authentication form was found')
|
149
|
|
150
|
def local_auth_check_post(self, username, password, select=None):
|
151
|
select = select or {}
|
152
|
url = self.host.auth_check_url
|
153
|
|
154
|
# Build request body
|
155
|
if self.host.post_parameters:
|
156
|
body_params = {}
|
157
|
# Login field
|
158
|
if self.host.post_parameters[self.host.login_field_name]['enabled'] is True:
|
159
|
body_params[self.host.login_field_name] = username
|
160
|
# Password field
|
161
|
if self.host.post_parameters[self.host.password_field_name]['enabled'] is True:
|
162
|
body_params[self.host.password_field_name] = password
|
163
|
# Select fields
|
164
|
for name, value in select.iteritems():
|
165
|
if self.host.post_parameters[name]['enabled'] is True:
|
166
|
body_params[name] = value
|
167
|
# Other fields (hidden, submit and custom)
|
168
|
for name, value in self.host.other_fields.iteritems():
|
169
|
if self.host.post_parameters[name]['enabled'] is True:
|
170
|
body_params[name] = self.host.post_parameters[name]['value']
|
171
|
body = urllib.urlencode(body_params)
|
172
|
else:
|
173
|
# XXX: Legacy (to be removed later) Send all parameters for sites configured with a previous version of Larpe
|
174
|
body = '%s=%s&%s=%s' % (self.host.login_field_name, username, self.host.password_field_name, password)
|
175
|
# Add select fields to the body
|
176
|
for name, value in select.iteritems():
|
177
|
body += '&%s=%s' % (name, value)
|
178
|
# Add hidden fields to the body
|
179
|
if self.host.send_hidden_fields:
|
180
|
for name, value in self.host.other_fields.iteritems():
|
181
|
body += '&%s=%s' % (name, value)
|
182
|
|
183
|
# Build request HTTP headers
|
184
|
if self.host.http_headers:
|
185
|
headers = {}
|
186
|
if self.host.http_headers['X-Forwarded-For']['enabled'] is True:
|
187
|
headers['X-Forwarded-For'] = get_request().get_environ('REMOTE_ADDR', '-')
|
188
|
for name, value in self.host.http_headers.iteritems():
|
189
|
if value['enabled'] is True and value['immutable'] is False:
|
190
|
headers[name] = value['value']
|
191
|
else:
|
192
|
# XXX: (to be removed later) Send default headers for sites configured with a previous version of Larpe
|
193
|
headers = { 'Content-Type': 'application/x-www-form-urlencoded',
|
194
|
'X-Forwarded-For': get_request().get_environ('REMOTE_ADDR', '-'),
|
195
|
'X-Forwarded-Host': self.host.reversed_hostname }
|
196
|
|
197
|
# Send request
|
198
|
response, status, data, auth_headers = http_post_request(url, body, headers, self.host.use_proxy)
|
199
|
|
200
|
cookies = response.getheader('Set-Cookie', None)
|
201
|
self.host.cookies = []
|
202
|
if cookies is not None:
|
203
|
cookies_list = []
|
204
|
cookies_set_list = []
|
205
|
for cookie in cookies.split(', '):
|
206
|
# Drop the path and other attributes
|
207
|
cookie_only = cookie.split('; ')[0]
|
208
|
regexp = re.compile('=')
|
209
|
if regexp.search(cookie_only) is None:
|
210
|
continue
|
211
|
# Split name and value
|
212
|
cookie_split = cookie_only.split('=')
|
213
|
cookie_name = cookie_split[0]
|
214
|
cookie_value = cookie_split[1]
|
215
|
cookies_list.append('%s=%s' % (cookie_name, cookie_value))
|
216
|
set_cookie = '%s=%s; path=/' % (cookie_name, cookie_value)
|
217
|
cookies_set_list.append(set_cookie)
|
218
|
self.host.cookies.append(cookie_name)
|
219
|
cookies_headers = '\r\nSet-Cookie: '.join(cookies_set_list)
|
220
|
get_response().set_header('Set-Cookie', cookies_headers)
|
221
|
self.host.store()
|
222
|
get_session().cookies = '; '.join(cookies_list)
|
223
|
else:
|
224
|
get_logger().warn('No cookie from local authentication')
|
225
|
|
226
|
return response.status, data
|
227
|
|
228
|
def local_auth_check_http_basic(self, username, password):
|
229
|
url = self.host.auth_form_url
|
230
|
hostname, query = urllib.splithost(url[5:])
|
231
|
conn = httplib.HTTPConnection(hostname)
|
232
|
|
233
|
auth_header = 'Basic %s' % base64.encodestring('%s:%s' % (username, password))
|
234
|
|
235
|
try:
|
236
|
conn.request('GET', query, headers={'Authorization': auth_header})
|
237
|
except socket.gaierror, err:
|
238
|
print err
|
239
|
conn.close()
|
240
|
return 0, None
|
241
|
else:
|
242
|
response = conn.getresponse()
|
243
|
conn.close()
|
244
|
return response.status, response.read()
|
245
|
|
246
|
def check_auth(self, status, data):
|
247
|
success = False
|
248
|
return_content = ''
|
249
|
|
250
|
# If status is 500, fail without checking other criterias
|
251
|
if status // 100 == 5:
|
252
|
success = False
|
253
|
return_content = redirect(self.host.get_return_url())
|
254
|
|
255
|
|
256
|
# For http auth, only check status code
|
257
|
elif self.host.auth_mode == 'http_basic':
|
258
|
# If failed, status code should be 401
|
259
|
if status // 100 == 2 or status // 100 == 3:
|
260
|
success = True
|
261
|
return_content = redirect(self.host.get_return_url())
|
262
|
|
263
|
else:
|
264
|
if self.host.auth_system == 'password':
|
265
|
# If there is a password field, authentication probably failed
|
266
|
regexp = re.compile("""<input[^>]*?type=["']?password["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
|
267
|
if not regexp.findall(data):
|
268
|
success = True
|
269
|
return_content = redirect(self.host.get_return_url())
|
270
|
elif self.host.auth_system == 'status':
|
271
|
match_status = int(self.host.auth_match_status)
|
272
|
if match_status == status:
|
273
|
success = True
|
274
|
return_content = redirect(self.host.get_return_url())
|
275
|
elif self.host.auth_system == 'match_text':
|
276
|
# If the auth_match_text is not matched, it means the authentication is successful
|
277
|
regexp = re.compile(self.host.auth_match_text, re.DOTALL)
|
278
|
if not regexp.findall(data):
|
279
|
success = True
|
280
|
return_content = redirect(self.host.get_return_url())
|
281
|
|
282
|
return success, return_content
|
283
|
|
284
|
def local_logout(self, federation=None, user=None, cookies=None):
|
285
|
if cookies is None and federation is None and user is not None:
|
286
|
federations = Federation.select(lambda x: user.name_identifiers[0] in x.name_identifiers)
|
287
|
if federations:
|
288
|
cookies = federations[0].cookies
|
289
|
|
290
|
# Logout request to the site
|
291
|
url = self.host.logout_url
|
292
|
if url is not None and cookies is not None:
|
293
|
try:
|
294
|
http_get_page(url, {'Cookie': cookies})
|
295
|
except ConnectionError, err:
|
296
|
get_logger().warning(_("%s logout failed") % url)
|
297
|
get_logger().debug(err)
|
298
|
|
299
|
# Remove cookies from the browser
|
300
|
# TODO: this should be removed because this only works
|
301
|
# with a 'direct' logout
|
302
|
if hasattr(self.host, 'cookies'):
|
303
|
for cookie in self.host.cookies:
|
304
|
get_response().expire_cookie(cookie, path='/')
|
305
|
|
306
|
def local_defederate(self, session, provider_id):
|
307
|
if session is None:
|
308
|
return
|
309
|
user = session.get_user(provider_id)
|
310
|
if user is not None:
|
311
|
federations = Federation.select(lambda x: user.name_identifiers[0] in x.name_identifiers)
|
312
|
for federation in federations:
|
313
|
self.local_logout(provider_id, federation)
|
314
|
federation.remove_name_identifier(user.name_identifiers[0])
|
315
|
federation.store()
|
316
|
|
317
|
def get_site_authentication(host):
|
318
|
if host.site_authentication_plugin is None:
|
319
|
return SiteAuthentication(host)
|
320
|
return site_authentication_plugins.get(host.site_authentication_plugin)(host)
|
321
|
|