1
|
"""
|
2
|
Dispatcher for basic auth form authentifications
|
3
|
"""
|
4
|
import Cookie
|
5
|
import base64
|
6
|
import copy
|
7
|
import re
|
8
|
import os
|
9
|
import traceback
|
10
|
import urllib
|
11
|
|
12
|
import mandaye
|
13
|
|
14
|
from cookielib import CookieJar
|
15
|
from datetime import datetime
|
16
|
from lxml.html import fromstring
|
17
|
from urlparse import parse_qs
|
18
|
|
19
|
from mandaye import config, __version__
|
20
|
from mandaye.exceptions import MandayeException
|
21
|
from mandaye.log import logger
|
22
|
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
|
23
|
from mandaye.response import _500, _302, _401
|
24
|
from mandaye.response import template_response
|
25
|
from mandaye.server import get_response
|
26
|
|
27
|
from mandaye.backends.default import backend
|
28
|
|
29
|
try:
|
30
|
from Crypto.Cipher import AES
|
31
|
except ImportError:
|
32
|
config.encrypt_sp_password = False
|
33
|
|
34
|
class AuthForm(object):
|
35
|
|
36
|
def __init__(self, env, mapper):
|
37
|
"""
|
38
|
env: WSGI environment
|
39
|
mapper: mapper's module like mandaye.mappers.linuxfr
|
40
|
"""
|
41
|
self.env = env
|
42
|
self.urls = mapper.urls
|
43
|
self.site_name = self.env["mandaye.config"]["site_name"]
|
44
|
self.form_values = mapper.form_values
|
45
|
if not self.form_values.has_key('form_headers'):
|
46
|
self.form_values['form_headers'] = {
|
47
|
'Content-Type': 'application/x-www-form-urlencoded',
|
48
|
'User-Agent': 'Mozilla/5.0 Mandaye/%s' % __version__
|
49
|
}
|
50
|
|
51
|
if not self.form_values.has_key('post_fields') or \
|
52
|
not self.form_values.has_key('username_field') or \
|
53
|
not self.form_values.has_key('login_url'):
|
54
|
logger.critical("Bad configuration: AuthForm form_values dict must have \
|
55
|
this keys: post_fields and username_field")
|
56
|
raise MandayeException, 'AuthForm bad configuration'
|
57
|
if not self.form_values.has_key('form_attrs') and \
|
58
|
not self.form_values.has_key('post_url'):
|
59
|
logger.critical("Bad configuration: you must set form_attrs or post_url")
|
60
|
if config.encrypt_secret and not self.form_values.has_key('password_field'):
|
61
|
logger.critical("Bad configuration: AuthForm form_values dict must have a \
|
62
|
a password_field key if you want to encode a password.")
|
63
|
raise MandayeException, 'AuthForm bad configuration'
|
64
|
|
65
|
self.login_url = self.form_values.get('login_url')
|
66
|
if not self.form_values.has_key('post_fields'):
|
67
|
self.form_values['post_fields'] = []
|
68
|
|
69
|
def get_default_mapping(self):
|
70
|
mapping = [
|
71
|
{
|
72
|
'path': r'/mandaye/logout$',
|
73
|
'on_response': [{'auth': 'slo'}]
|
74
|
},
|
75
|
]
|
76
|
if config.a2_auto_connection:
|
77
|
mapping.append({
|
78
|
'path': r'/',
|
79
|
'response': {
|
80
|
'filter': self.auto_connection,
|
81
|
'condition': self.is_connected_a2
|
82
|
}
|
83
|
})
|
84
|
return mapping
|
85
|
|
86
|
def encrypt_pwd(self, password):
|
87
|
""" This method allows you to encrypt a password
|
88
|
To use this feature you muste set encrypt_sp_password to True
|
89
|
in your configuration and set a secret in encrypt_secret
|
90
|
|
91
|
Return encrypted password
|
92
|
"""
|
93
|
if config.encrypt_secret:
|
94
|
logger.debug("Encrypt password")
|
95
|
try:
|
96
|
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
|
97
|
password = cipher.encrypt(password)
|
98
|
password = base64.b64encode(password)
|
99
|
return password
|
100
|
except Exception, e:
|
101
|
if config.debug:
|
102
|
traceback.print_exc()
|
103
|
logger.warning('Password encrypting failed %s' % e)
|
104
|
else:
|
105
|
logger.warning("You must set a secret to use pwd encryption")
|
106
|
|
107
|
def decrypt_pwd(self, password):
|
108
|
""" This method allows you to dencrypt a password encrypt with
|
109
|
encrypt_pwd method. To use this feature you muste set
|
110
|
encrypt_sp_password to True in your configuration and
|
111
|
set a secret in encrypt_secret
|
112
|
|
113
|
Return decrypted password
|
114
|
"""
|
115
|
if config.encrypt_secret:
|
116
|
logger.debug("Decrypt password")
|
117
|
try:
|
118
|
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
|
119
|
password = base64.b64decode(password)
|
120
|
password = cipher.decrypt(password)
|
121
|
return password
|
122
|
except Exception, e:
|
123
|
if config.debug:
|
124
|
traceback.print_exc()
|
125
|
logger.warning('Decrypting password failed: %r', e)
|
126
|
else:
|
127
|
logger.warning("You must set a secret to use pwd decryption")
|
128
|
|
129
|
def get_current_unique_id(self, env):
|
130
|
if env['beaker.session'].has_key('unique_id'):
|
131
|
return env['beaker.session']['unique_id']
|
132
|
return None
|
133
|
|
134
|
def replay(self, env, post_values):
|
135
|
""" replay the login / password
|
136
|
env: WSGI env with beaker session and the target
|
137
|
post_values: dict with the field name (key) and the field value (value)
|
138
|
"""
|
139
|
logger.debug("authform.replay post_values: %r", post_values)
|
140
|
cj = CookieJar()
|
141
|
request = HTTPRequest()
|
142
|
action = self.form_values.get('post_url')
|
143
|
auth_form = None
|
144
|
# if there is a form parse it
|
145
|
if not "://" in self.login_url:
|
146
|
self.login_url = os.path.join(env['target'].geturl(), self.login_url)
|
147
|
login = get_response(env, request, self.login_url, cj)
|
148
|
if login.code == 502:
|
149
|
return login
|
150
|
if self.form_values.has_key('form_attrs'):
|
151
|
html = fromstring(login.msg)
|
152
|
for form in html.forms:
|
153
|
is_good = True
|
154
|
for key, value in self.form_values['form_attrs'].iteritems():
|
155
|
if form.get(key) != value:
|
156
|
is_good = False
|
157
|
if is_good:
|
158
|
auth_form = form
|
159
|
break
|
160
|
|
161
|
if auth_form == None:
|
162
|
logger.critical("%r %r: can't find login form on %r",
|
163
|
env['HTTP_HOST'], env['PATH_INFO'], self.login_url)
|
164
|
return _500(env['PATH_INFO'], "Replay: Can't find login form")
|
165
|
params = {}
|
166
|
for input in auth_form.inputs:
|
167
|
if input.name and input.type != 'button':
|
168
|
if input.value:
|
169
|
params[input.name] = input.value.encode('utf-8')
|
170
|
else:
|
171
|
params[input.name] = ''
|
172
|
for key, value in post_values.iteritems():
|
173
|
params[key] = value
|
174
|
else:
|
175
|
params = post_values
|
176
|
|
177
|
if not self.form_values.has_key('post_url'):
|
178
|
if len(auth_form) and not auth_form.action:
|
179
|
logger.critical("%r %r: don't find form action on %r",
|
180
|
env['HTTP_HOST'], env['PATH_INFO'], self.login_url)
|
181
|
return _500(env['PATH_INFO'], 'Replay: form action not found')
|
182
|
action = auth_form.action
|
183
|
|
184
|
if not "://" in action:
|
185
|
login_url = re.sub(r'\?.*$', '', self.login_url)
|
186
|
action = os.path.join(login_url, action)
|
187
|
|
188
|
cookies = login.cookies
|
189
|
headers = HTTPHeader()
|
190
|
headers.load_from_dict(self.form_values['form_headers'])
|
191
|
params = urllib.urlencode(params)
|
192
|
request = HTTPRequest(cookies, headers, "POST", params)
|
193
|
return get_response(env, request, action, cj)
|
194
|
|
195
|
def _save_association(self, env, unique_id, post_values):
|
196
|
""" save an association in the database
|
197
|
env: wsgi environment
|
198
|
unique_id: idp uinique id
|
199
|
post_values: dict with the post values
|
200
|
"""
|
201
|
logger.debug('AuthForm._save_association: save a new association')
|
202
|
sp_login = post_values[self.form_values['username_field']]
|
203
|
if config.encrypt_sp_password:
|
204
|
password = self.encrypt_pwd(post_values[self.form_values['password_field']])
|
205
|
post_values[self.form_values['password_field']] = password
|
206
|
service_provider = backend.ManagerServiceProvider.get_or_create(self.site_name)
|
207
|
idp_user = backend.ManagerIDPUser.get_or_create(unique_id)
|
208
|
sp_user = backend.ManagerSPUser.get(sp_login, idp_user, service_provider)
|
209
|
if sp_user:
|
210
|
sp_user.post_values = post_values
|
211
|
backend.ManagerSPUser.save()
|
212
|
else:
|
213
|
sp_user = backend.ManagerSPUser.create(sp_login, post_values,
|
214
|
idp_user, service_provider)
|
215
|
env['beaker.session']['unique_id'] = unique_id
|
216
|
env['beaker.session'][self.site_name] = sp_user.id
|
217
|
env['beaker.session'].save()
|
218
|
|
219
|
def associate_submit(self, env, values, request, response):
|
220
|
""" Associate your login / password into your database
|
221
|
"""
|
222
|
logger.debug("Trying to associate a user")
|
223
|
unique_id = env['beaker.session'].get('unique_id')
|
224
|
if request.msg:
|
225
|
if not unique_id:
|
226
|
logger.warning("Association failed: user isn't login on Mandaye")
|
227
|
return _302(self.urls.get('connection_url'))
|
228
|
if type(request.msg) == str:
|
229
|
post = parse_qs(request.msg, request)
|
230
|
else:
|
231
|
post = parse_qs(request.msg.read(), request)
|
232
|
logger.debug("association post: %r", post)
|
233
|
qs = parse_qs(env['QUERY_STRING'])
|
234
|
for key, value in qs.iteritems():
|
235
|
qs[key] = value[0]
|
236
|
post_fields = self.form_values['post_fields']
|
237
|
post_values = {}
|
238
|
for field in post_fields:
|
239
|
if not post.has_key(field):
|
240
|
logger.info('Association auth failed: form not correctly filled')
|
241
|
logger.info('%r is missing', field)
|
242
|
qs['type'] = 'badlogin'
|
243
|
return _302(self.urls.get('associate_url') + "?%s" % urllib.urlencode(qs))
|
244
|
post_values[field] = post[field][0]
|
245
|
response = self.replay(env, post_values)
|
246
|
if eval(values['condition']):
|
247
|
logger.debug("Replay works: save the association")
|
248
|
self._save_association(env, unique_id, post_values)
|
249
|
if qs.has_key('next_url'):
|
250
|
return _302(qs['next_url'], response.cookies)
|
251
|
return response
|
252
|
logger.info('Auth failed: Bad password or login')
|
253
|
qs['type'] = 'badlogin'
|
254
|
return _302(self.urls.get('associate_url') + "?%s" % urllib.urlencode(qs))
|
255
|
|
256
|
def _login_sp_user(self, sp_user, env, condition, values):
|
257
|
""" Log in sp user
|
258
|
"""
|
259
|
if not sp_user.login:
|
260
|
return _500(env['PATH_INFO'],
|
261
|
'Invalid values for AuthFormDispatcher.login')
|
262
|
post_values = copy.copy(sp_user.post_values)
|
263
|
if config.encrypt_sp_password:
|
264
|
password = self.decrypt_pwd(post_values[self.form_values['password_field']])
|
265
|
post_values[self.form_values['password_field']] = password
|
266
|
response = self.replay(env, post_values)
|
267
|
qs = parse_qs(env['QUERY_STRING'])
|
268
|
if condition and eval(condition):
|
269
|
sp_user.last_connection = datetime.now()
|
270
|
backend.ManagerSPUser.save()
|
271
|
env['beaker.session'][self.site_name] = sp_user.id
|
272
|
env['beaker.session'].save()
|
273
|
if qs.has_key('next_url'):
|
274
|
return _302(qs['next_url'][0], response.cookies)
|
275
|
else:
|
276
|
return response
|
277
|
else:
|
278
|
return _302(self.urls.get('associate_url') + "?type=failed")
|
279
|
|
280
|
def login(self, env, values, request, response):
|
281
|
""" Automatic login on a site with a form
|
282
|
"""
|
283
|
# Specific method to get current idp unique id
|
284
|
unique_id = self.get_current_unique_id(env)
|
285
|
logger.debug('Trying to login on Mandaye')
|
286
|
if not unique_id:
|
287
|
return _401('Access denied: invalid token')
|
288
|
|
289
|
# FIXME: hack to force beaker to generate an id
|
290
|
# somtimes beaker doesn't do it by himself
|
291
|
env['beaker.session'].regenerate_id()
|
292
|
|
293
|
env['beaker.session']['unique_id'] = unique_id
|
294
|
env['beaker.session'].save()
|
295
|
|
296
|
logger.debug('User %s successfully login' % env['beaker.session']['unique_id'])
|
297
|
|
298
|
idp_user = backend.ManagerIDPUser.get_or_create(unique_id)
|
299
|
service_provider = backend.ManagerServiceProvider.get_or_create(self.site_name)
|
300
|
sp_user = backend.ManagerSPUser.get_last_connected(idp_user, service_provider)
|
301
|
if not sp_user:
|
302
|
logger.debug('User %s is not associate' % env['beaker.session']['unique_id'])
|
303
|
return _302(self.urls.get('associate_url') + "?type=first")
|
304
|
return self._login_sp_user(sp_user, env, values['condition'], values)
|
305
|
|
306
|
def logout(self, env, values, request, response):
|
307
|
""" Destroy the Beaker session
|
308
|
"""
|
309
|
logger.debug('Logout from Mandaye')
|
310
|
env['beaker.session'].delete()
|
311
|
return response
|
312
|
|
313
|
def auto_connection(self, env, values, request, response):
|
314
|
connection_url = self.urls["connection_url"]
|
315
|
logger.debug("Redirection using url : %s" % connection_url)
|
316
|
return _302(connection_url)
|
317
|
|
318
|
def local_logout(self, env, values, request, response):
|
319
|
logger.info('SP logout initiated by Mandaye')
|
320
|
self.logout(env, values, request, response)
|
321
|
|
322
|
next_url = None
|
323
|
qs = parse_qs(env['QUERY_STRING'])
|
324
|
if qs.has_key('RelayState'):
|
325
|
next_url = qs['RelayState'][0]
|
326
|
elif qs.has_key('next_url'):
|
327
|
next_url = qs['next_url'][0]
|
328
|
elif values.has_key('next_url'):
|
329
|
next_url = values['next_url']
|
330
|
|
331
|
req_cookies = request.cookies
|
332
|
for cookie in req_cookies.values():
|
333
|
cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
|
334
|
if next_url:
|
335
|
return _302(next_url, req_cookies)
|
336
|
else:
|
337
|
return _302('/', req_cookies)
|
338
|
|
339
|
def change_user(self, env, values, request, response):
|
340
|
""" Multi accounts feature
|
341
|
Change the current login user
|
342
|
You must call this method into a response filter
|
343
|
This method must have a query string with a username parameter
|
344
|
"""
|
345
|
# TODO: need to logout the first
|
346
|
unique_id = env['beaker.session']['unique_id']
|
347
|
qs = parse_qs(env['QUERY_STRING'])
|
348
|
if not qs.has_key('id') and not unique_id:
|
349
|
return _401('Access denied: beaker session invalid or not qs id')
|
350
|
if qs.has_key('id'):
|
351
|
id = qs['id'][0]
|
352
|
sp_user = backend.ManagerSPUser.get_by_id(id)
|
353
|
else:
|
354
|
service_provider = backend.ManagerServiceProvider.get(self.site_name)
|
355
|
idp_user = backend.ManagerIDPUser.get(unique_id)
|
356
|
sp_user = backend.ManagerSPUser.get_last_connected(idp_user, service_provider)
|
357
|
if not sp_user:
|
358
|
return _302(self.urls.get('associate_url'))
|
359
|
return self._login_sp_user(sp_user, env, 'response.code==302', values)
|
360
|
|
361
|
def disassociate(self, env, values, request, response):
|
362
|
""" Disassociate an account with the Mandaye account
|
363
|
You need to put the id of the sp user you want to disassociate
|
364
|
in the query string (..?id=42) or use by service provider name
|
365
|
(..?sp_name=)
|
366
|
"""
|
367
|
if env['beaker.session'].has_key('unique_id'):
|
368
|
unique_id = env['beaker.session']['unique_id']
|
369
|
else:
|
370
|
return _401('Access denied: no session')
|
371
|
qs = parse_qs(env['QUERY_STRING'])
|
372
|
if values.get('next_url'):
|
373
|
next_url = values.get('next_url')
|
374
|
else:
|
375
|
next_url = '/'
|
376
|
if qs.has_key('next_url'):
|
377
|
next_url = qs['next_url'][0]
|
378
|
if qs.has_key('id'):
|
379
|
sp_id = qs['id'][0]
|
380
|
sp_user = backend.ManagerSPUser.get_by_id(sp_id)
|
381
|
if sp_user:
|
382
|
backend.ManagerSPUser.delete(sp_user)
|
383
|
if backend.ManagerSPUser.get_sp_users(unique_id, self.site_name):
|
384
|
env['QUERY_STRING'] = ''
|
385
|
return self.change_user(env, values, request, response)
|
386
|
else:
|
387
|
return _401('Access denied: bad id')
|
388
|
elif qs.has_key('sp_name'):
|
389
|
sp_name = qs['sp_name'][0]
|
390
|
for sp_user in \
|
391
|
backend.ManagerSPUser.get_sp_users(unique_id, sp_name):
|
392
|
backend.ManagerSPUser.delete(sp_user)
|
393
|
else:
|
394
|
return _401('Access denied: no id or sp name')
|
395
|
values['next_url'] = next_url
|
396
|
if qs.has_key('logout'):
|
397
|
return self.local_logout(env, values, request, response)
|
398
|
return _302(next_url)
|
399
|
|
400
|
def is_connected_a2(self, env, request, response):
|
401
|
""" Auto connection only which works only with Authentic2
|
402
|
"""
|
403
|
if request.cookies.has_key('A2_OPENED_SESSION') and\
|
404
|
not env['beaker.session'].has_key('unique_id'):
|
405
|
logger.info('Trying an auto connection')
|
406
|
return True
|
407
|
return False
|
408
|
|