Projet

Général

Profil

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

root / mandaye / auth / authform.py @ 458b278a

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

    
(2-2/3)