Projet

Général

Profil

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

root / mandaye / auth / authform.py @ 2f55d335

1
"""
2
Dispatcher for basic auth form authentifications
3
"""
4
import Cookie
5
import base64
6
import copy
7
import re
8
import traceback
9
import urllib
10

    
11
import mandaye
12

    
13
from cookielib import CookieJar
14
from datetime import datetime
15
from lxml.html import fromstring
16
from urlparse import parse_qs
17

    
18
from mandaye import config, __version__
19
from mandaye.exceptions import MandayeException
20
from mandaye.log import logger
21
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
22
from mandaye.response import _500, _302, _401
23
from mandaye.response import template_response
24
from mandaye.server import get_response
25

    
26
from mandaye.backends.default import backend
27

    
28
try:
29
    from Crypto.Cipher import AES
30
except ImportError:
31
    config.encrypt_sp_password = False
32

    
33
class AuthForm(object):
34

    
35
    def __init__(self, env, mapper):
36
        """
37
        env: WSGI environment
38
        mapper: mapper's module like mandaye.mappers.linuxfr
39
        """
40
        self.env = env
41
        self.urls = mapper.urls
42
        self.site_name = self.env["mandaye.config"]["site_name"]
43
        self.form_values = mapper.form_values
44
        if not self.form_values.has_key('form_headers'):
45
            self.form_values['form_headers'] = {
46
                    'Content-Type': 'application/x-www-form-urlencoded',
47
                    'User-Agent': 'Mozilla/5.0 Mandaye/%s' % __version__
48
                    }
49

    
50
        if not self.form_values.has_key('post_fields') or \
51
                not self.form_values.has_key('username_field') or \
52
                not self.form_values.has_key('login_url'):
53
            logger.critical("Bad configuration: AuthForm form_values dict must have \
54
this keys: post_fields and username_field")
55
            raise MandayeException, 'AuthForm bad configuration'
56
        if not self.form_values.has_key('form_attrs') and \
57
                not self.form_values.has_key('post_url'):
58
            logger.critical("Bad configuration: you must set form_attrs or post_url")
59
        if config.encrypt_secret and not self.form_values.has_key('password_field'):
60
            logger.critical("Bad configuration: AuthForm form_values dict must have a \
61
a password_field key if you want to encode a password.")
62
            raise MandayeException, 'AuthForm bad configuration'
63

    
64
        self.login_url = self.form_values.get('login_url')
65
        if not self.form_values.has_key('post_fields'):
66
            self.form_values['post_fields'] = []
67

    
68
    def get_default_mapping(self):
69
        mapping = [
70
                {
71
                    'path': r'/mandaye/logout$',
72
                    'on_response': [{'auth': 'slo'}]
73
                    },
74
                ]
75
        if config.a2_auto_connection:
76
            mapping.append({
77
                'path': r'/',
78
                'response': {
79
                    'filter': self.auto_connection,
80
                    'condition': self.is_connected_a2
81
                    }
82
                })
83
        return mapping
84

    
85
    def encrypt_pwd(self, password):
86
        """ This method allows you to encrypt a password
87
        To use this feature you muste set encrypt_sp_password to True
88
        in your configuration and set a secret in encrypt_secret
89

    
90
        Return encrypted password
91
        """
92
        if config.encrypt_secret:
93
            logger.debug("Encrypt password")
94
            try:
95
                cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
96
                password = cipher.encrypt(password)
97
                password = base64.b64encode(password)
98
                return password
99
            except Exception, e:
100
                if config.debug:
101
                    traceback.print_exc()
102
                logger.warning('Password encrypting failed %s' % e)
103
        else:
104
            logger.warning("You must set a secret to use pwd encryption")
105

    
106
    def decrypt_pwd(self, password):
107
        """ This method allows you to dencrypt a password encrypt with
108
        encrypt_pwd method. To use this feature you muste set
109
        encrypt_sp_password to True in your configuration and
110
        set a secret in encrypt_secret
111

    
112
        Return decrypted password
113
        """
114
        if config.encrypt_secret:
115
            logger.debug("Decrypt password")
116
            try:
117
                cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
118
                password = base64.b64decode(password)
119
                password = cipher.decrypt(password)
120
                return password
121
            except Exception, e:
122
                if config.debug:
123
                    traceback.print_exc()
124
                logger.warning('Decrypting password failed: %r', e)
125
        else:
126
            logger.warning("You must set a secret to use pwd decryption")
127

    
128
    def get_current_unique_id(self, env):
129
        if env['beaker.session'].has_key('unique_id'):
130
            return env['beaker.session']['unique_id']
131
        return None
132

    
133
    def replay(self, env, post_values):
134
        """ replay the login / password
135
        env: WSGI env with beaker session and the target
136
        post_values: dict with the field name (key) and the field value (value)
137
        """
138
        logger.debug("authform.replay post_values: %r", post_values)
139
        cj = CookieJar()
140
        request = HTTPRequest()
141
        action = self.form_values.get('post_url')
142
        auth_form = None
143
        # if there is a form parse it
144
        if not "://" in self.login_url:
145
            self.login_url = env['target'].geturl() + self.login_url
146
        login = get_response(env, request, self.login_url, cj)
147
        if login.code == 502:
148
            return login
149
        if self.form_values.has_key('form_attrs'):
150
            html = fromstring(login.msg)
151
            for form in html.forms:
152
                is_good = True
153
                for key, value in self.form_values['form_attrs'].iteritems():
154
                    if form.get(key) != value:
155
                        is_good = False
156
                if is_good:
157
                    auth_form = form
158
                    break
159

    
160
            if auth_form == None:
161
                logger.critical("%r %r: can't find login form on %r",
162
                        env['HTTP_HOST'], env['PATH_INFO'], self.login_url)
163
                return _500(env['PATH_INFO'], "Replay: Can't find login form")
164
            params = {}
165
            for input in auth_form.inputs:
166
                if input.name and input.type != 'button':
167
                    if input.value:
168
                        params[input.name] = input.value.encode('utf-8')
169
                    else:
170
                        params[input.name] = ''
171
            for key, value in post_values.iteritems():
172
                params[key] = value
173
        else:
174
            params = post_values
175

    
176
        if not self.form_values.has_key('post_url'):
177
            if len(auth_form) and not auth_form.action:
178
                logger.critical("%r %r: don't find form action on %r",
179
                        env['HTTP_HOST'], env['PATH_INFO'], self.login_url)
180
                return _500(env['PATH_INFO'], 'Replay: form action not found')
181
            action = auth_form.action
182

    
183
        if not "://" in action:
184
            action = re.sub(r'(.+)/.*(\?.+)$', r'\1/%s\2' % action, self.login_url)
185

    
186
        cookies = login.cookies
187
        headers = HTTPHeader()
188
        headers.load_from_dict(self.form_values['form_headers'])
189
        params = urllib.urlencode(params)
190
        request = HTTPRequest(cookies, headers, "POST", params)
191
        return get_response(env, request, action, cj)
192

    
193
    def _save_association(self, env, unique_id, post_values):
194
        """ save an association in the database
195
        env: wsgi environment
196
        unique_id: idp uinique id
197
        post_values: dict with the post values
198
        """
199
        logger.debug('AuthForm._save_association: save a new association')
200
        sp_login = post_values[self.form_values['username_field']]
201
        if config.encrypt_sp_password:
202
            password = self.encrypt_pwd(post_values[self.form_values['password_field']])
203
            post_values[self.form_values['password_field']] = password
204
        service_provider = backend.ManagerServiceProvider.get_or_create(self.site_name)
205
        idp_user = backend.ManagerIDPUser.get_or_create(unique_id)
206
        sp_user = backend.ManagerSPUser.get(sp_login, idp_user, service_provider)
207
        if sp_user:
208
            sp_user.post_values = post_values
209
            backend.ManagerSPUser.save()
210
        else:
211
            sp_user = backend.ManagerSPUser.create(sp_login, post_values,
212
                    idp_user, service_provider)
213
        env['beaker.session']['unique_id'] = unique_id
214
        env['beaker.session'][self.site_name] = sp_user.id
215
        env['beaker.session'].save()
216

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

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

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

    
287
        # FIXME: hack to force beaker to generate an id
288
        # somtimes beaker doesn't do it by himself
289
        env['beaker.session'].regenerate_id()
290

    
291
        env['beaker.session']['unique_id'] = unique_id
292
        env['beaker.session'].save()
293

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

    
296
        idp_user = backend.ManagerIDPUser.get_or_create(unique_id)
297
        service_provider = backend.ManagerServiceProvider.get_or_create(self.site_name)
298
        sp_user = backend.ManagerSPUser.get_last_connected(idp_user, service_provider)
299
        if not sp_user:
300
            logger.debug('User %s is not associate' % env['beaker.session']['unique_id'])
301
            return _302(self.urls.get('associate_url') + "?type=first")
302
        return self._login_sp_user(sp_user, env, values['condition'], values)
303

    
304
    def logout(self, env, values, request, response):
305
        """ Destroy the Beaker session
306
        """
307
        logger.debug('Logout from Mandaye')
308
        env['beaker.session'].delete()
309
        return response
310

    
311
    def auto_connection(self, env, values, request, response):
312
        connection_url = self.urls["connection_url"]
313
        logger.debug("Redirection using url : %s" % connection_url)
314
        return _302(connection_url)
315

    
316
    def local_logout(self, env, values, request, response):
317
        logger.info('SP logout initiated by Mandaye')
318
        self.logout(env, values, request, response)
319

    
320
        next_url = None
321
        qs = parse_qs(env['QUERY_STRING'])
322
        if qs.has_key('RelayState'):
323
            next_url = qs['RelayState'][0]
324
        elif qs.has_key('next_url'):
325
            next_url = qs['next_url'][0]
326
        elif values.has_key('next_url'):
327
            next_url = values['next_url']
328

    
329
        req_cookies = request.cookies
330
        for cookie in req_cookies.values():
331
            cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
332
            cookie['path'] = '/'
333
        if next_url:
334
            return _302(next_url, req_cookies)
335
        else:
336
            return _302('/', req_cookies)
337

    
338
    def change_user(self, env, values, request, response):
339
        """ Multi accounts feature
340
        Change the current login user
341
        You must call this method into a response filter
342
        This method must have a query string with a username parameter
343
        """
344
        # TODO: need to logout the first
345
        unique_id = env['beaker.session']['unique_id']
346
        qs = parse_qs(env['QUERY_STRING'])
347
        if not qs.has_key('id') and not unique_id:
348
            return _401('Access denied: beaker session invalid or not qs id')
349
        if qs.has_key('id'):
350
            id = qs['id'][0]
351
            sp_user = backend.ManagerSPUser.get_by_id(id)
352
        else:
353
            service_provider = backend.ManagerServiceProvider.get(self.site_name)
354
            idp_user = backend.ManagerIDPUser.get(unique_id)
355
            sp_user = backend.ManagerSPUser.get_last_connected(idp_user, service_provider)
356
        if not sp_user:
357
            return _302(self.urls.get('associate_url'))
358
        return self._login_sp_user(sp_user, env, 'response.code==302', values)
359

    
360
    def disassociate(self, env, values, request, response):
361
        """ Disassociate an account with the Mandaye account
362
        You need to put the id of the sp user you want to disassociate
363
        in the query string (..?id=42) or use by service provider name
364
        (..?sp_name=)
365
        """
366
        if env['beaker.session'].has_key('unique_id'):
367
            unique_id = env['beaker.session']['unique_id']
368
        else:
369
            return _401('Access denied: no session')
370
        qs = parse_qs(env['QUERY_STRING'])
371
        if values.get('next_url'):
372
            next_url = values.get('next_url')
373
        else:
374
            next_url = '/'
375
        if qs.has_key('next_url'):
376
            next_url = qs['next_url'][0]
377
        if qs.has_key('id'):
378
            sp_id = qs['id'][0]
379
            sp_user = backend.ManagerSPUser.get_by_id(sp_id)
380
            if sp_user:
381
                backend.ManagerSPUser.delete(sp_user)
382
                if backend.ManagerSPUser.get_sp_users(unique_id, self.site_name):
383
                    env['QUERY_STRING'] = ''
384
                    return self.change_user(env, values, request, response)
385
            else:
386
                return _401('Access denied: bad id')
387
        elif qs.has_key('sp_name'):
388
            sp_name = qs['sp_name'][0]
389
            for sp_user in \
390
                    backend.ManagerSPUser.get_sp_users(unique_id, sp_name):
391
                backend.ManagerSPUser.delete(sp_user)
392
        else:
393
            return _401('Access denied: no id or sp name')
394
        values['next_url'] = next_url
395
        if qs.has_key('logout'):
396
            return self.local_logout(env, values, request, response)
397
        return _302(next_url)
398

    
399
    def is_connected_a2(self, env, request, response):
400
        """ Auto connection only which works only with Authentic2
401
        """
402
        if request.cookies.has_key('A2_OPENED_SESSION') and\
403
                not env['beaker.session'].has_key('unique_id'):
404
            logger.info('Trying an auto connection')
405
            return True
406
        return False
407

    
(2-2/3)