Projet

Général

Profil

Télécharger (16 ko) Statistiques
| Branche: | Tag: | Révision:

root / mandaye / auth / authform.py @ c62aae38

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 Association
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

    
207
        asso_id = Association.update_or_create(self.site_name, sp_login,
208
                post_values, unique_id)
209
        env['beaker.session']['unique_id'] = unique_id
210
        env['beaker.session'][self.site_name] = asso_id
211
        env['beaker.session'].save()
212

    
213
    def associate_submit(self, env, values, request, response):
214
        """  Associate your login / password into your database
215
        """
216
        logger.debug("Trying to associate a user")
217
        unique_id = env['beaker.session'].get('unique_id')
218
        if request.msg:
219
            if not unique_id:
220
                logger.warning("Association failed: user isn't login on Mandaye")
221
                return _302(self.urls.get('connection_url'))
222
            if type(request.msg) == str:
223
                post = parse_qs(request.msg, request)
224
            else:
225
                post = parse_qs(request.msg.read(), request)
226
            logger.debug("association post: %r", post)
227
            qs = parse_qs(env['QUERY_STRING'])
228
            for key, value in qs.iteritems():
229
                qs[key] = value[0]
230
            post_fields = self.form_values['post_fields']
231
            post_values = {}
232
            for field in post_fields:
233
                if not post.has_key(field):
234
                    logger.info('Association auth failed: form not correctly filled')
235
                    logger.info('%r is missing', field)
236
                    qs['type'] = 'badlogin'
237
                    return _302(self.urls.get('associate_url') + "?%s" % urllib.urlencode(qs))
238
                post_values[field] = post[field][0]
239
            response = self.replay(env, post_values)
240
            if eval(values['condition']):
241
                logger.debug("Replay works: save the association")
242
                self._save_association(env, unique_id, post_values)
243
                if qs.has_key('next_url'):
244
                    return _302(qs['next_url'], response.cookies)
245
                return response
246
            logger.info('Auth failed: Bad password or login')
247
            qs['type'] = 'badlogin'
248
            return _302(self.urls.get('associate_url') + "?%s" % urllib.urlencode(qs))
249

    
250
    def _login_sp_user(self, association, env, condition, values):
251
        """ Log in sp user
252
        """
253
        if not association['sp_login']:
254
            return _500(env['PATH_INFO'],
255
                    'Invalid values for AuthFormDispatcher.login')
256
        post_values = copy.copy(association['sp_post_values'])
257
        if config.encrypt_sp_password:
258
            password = self.decrypt_pwd(post_values[self.form_values['password_field']])
259
            post_values[self.form_values['password_field']] = password
260
        response = self.replay(env, post_values)
261
        qs = parse_qs(env['QUERY_STRING'])
262
        if condition and eval(condition):
263
            Association.update_last_connection(association['id'])
264
            env['beaker.session'][self.site_name] = association['id']
265
            env['beaker.session'].save()
266
            if qs.has_key('next_url'):
267
                return _302(qs['next_url'][0], response.cookies)
268
            else:
269
                return response
270
        else:
271
            return _302(self.urls.get('associate_url') + "?type=failed")
272

    
273
    def login(self, env, values, request, response):
274
        """ Automatic login on a site with a form
275
        """
276
        # Specific method to get current idp unique id
277
        unique_id = self.get_current_unique_id(env)
278
        logger.debug('Trying to login on Mandaye')
279
        if not unique_id:
280
            return _401('Access denied: invalid token')
281

    
282
        # FIXME: hack to force beaker to generate an id
283
        # somtimes beaker doesn't do it by himself
284
        env['beaker.session'].regenerate_id()
285

    
286
        env['beaker.session']['unique_id'] = unique_id
287
        env['beaker.session'].save()
288

    
289
        logger.debug('User %s successfully login' % env['beaker.session']['unique_id'])
290

    
291
        association = Association.get_last_connected(self.site_name, unique_id)
292
        if not association:
293
            logger.debug('User %s is not associate' % env['beaker.session']['unique_id'])
294
            return _302(self.urls.get('associate_url') + "?type=first")
295
        return self._login_sp_user(association, env, values['condition'], values)
296

    
297
    def logout(self, env, values, request, response):
298
        """ Destroy the Beaker session
299
        """
300
        logger.debug('Logout from Mandaye')
301
        env['beaker.session'].delete()
302
        return response
303

    
304
    def auto_connection(self, env, values, request, response):
305
        connection_url = self.urls["connection_url"]
306
        logger.debug("Redirection using url : %s" % connection_url)
307
        return _302(connection_url)
308

    
309
    def local_logout(self, env, values, request, response):
310
        logger.info('SP logout initiated by Mandaye')
311
        self.logout(env, values, request, response)
312

    
313
        next_url = None
314
        qs = parse_qs(env['QUERY_STRING'])
315
        if qs.has_key('RelayState'):
316
            next_url = qs['RelayState'][0]
317
        elif qs.has_key('next_url'):
318
            next_url = qs['next_url'][0]
319
        elif values.has_key('next_url'):
320
            next_url = values['next_url']
321

    
322
        req_cookies = request.cookies
323
        for cookie in req_cookies.values():
324
            cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
325
        if next_url:
326
            return _302(next_url, req_cookies)
327
        else:
328
            return _302('/', req_cookies)
329

    
330
    def change_user(self, env, values, request, response):
331
        """ Multi accounts feature
332
        Change the current login user
333
        You must call this method into a response filter
334
        This method must have a query string with a username parameter
335
        """
336
        # TODO: need to logout the first
337
        unique_id = env['beaker.session']['unique_id']
338
        qs = parse_qs(env['QUERY_STRING'])
339
        if not qs.has_key('id') and not unique_id:
340
            return _401('Access denied: beaker session invalid or not qs id')
341
        if qs.has_key('id'):
342
            asso_id = qs['id'][0]
343
            association = Association.get_by_id(asso_id)
344
        else:
345
            association = Association.get_last_connected(self.site_name, unique_id)
346
        if not association:
347
            return _302(self.urls.get('associate_url'))
348
        return self._login_sp_user(association, env, 'response.code==302', values)
349

    
350
    def disassociate(self, env, values, request, response):
351
        """ Disassociate an account with the Mandaye account
352
        You need to put the id of the sp user you want to disassociate
353
        in the query string (..?id=42) or use by service provider name
354
        (..?sp_name=)
355
        """
356
        if env['beaker.session'].has_key('unique_id'):
357
            unique_id = env['beaker.session']['unique_id']
358
        else:
359
            return _401('Access denied: no session')
360
        qs = parse_qs(env['QUERY_STRING'])
361
        if values.get('next_url'):
362
            next_url = values.get('next_url')
363
        else:
364
            next_url = '/'
365
        if qs.has_key('next_url'):
366
            next_url = qs['next_url'][0]
367
        if qs.has_key('id'):
368
            asso_id = qs['id'][0]
369
            if Association.has_id(asso_id):
370
                Association.delete(asso_id)
371
                if Association.get(self.site_name, unique_id):
372
                    env['QUERY_STRING'] = ''
373
                    return self.change_user(env, values, request, response)
374
            else:
375
                return _401('Access denied: bad id')
376
        elif qs.has_key('sp_name'):
377
            sp_name = qs['sp_name'][0]
378
            for asso in \
379
                    Association.get(sp_name, unique_id):
380
                Association.delete(asso['id'])
381
        else:
382
            return _401('Access denied: no id or sp name')
383
        values['next_url'] = next_url
384
        if qs.has_key('logout'):
385
            return self.local_logout(env, values, request, response)
386
        return _302(next_url)
387

    
388
    def is_connected_a2(self, env, request, response):
389
        """ Auto connection only which works only with Authentic2
390
        """
391
        if request.cookies.has_key('A2_OPENED_SESSION') and\
392
                not env['beaker.session'].has_key('unique_id'):
393
            logger.info('Trying an auto connection')
394
            return True
395
        return False
396

    
(2-2/3)