Projet

Général

Profil

« Précédent | Suivant » 

Révision 6b178829

Ajouté par Jérôme Schneider il y a presque 12 ans

Fix #709: authform now store every post values

  • README.rst: upgrade dependencies * mandaye/__init__.py: switch to 0.3 version * mandaye/auth/authform.py: use post values instead of password and
    username * mandaye/models.py: add dict management and post_values argument * mandaye/configs: upgrade configurations * mandaye/templates/ : upgrade templates for this new feature

Voir les différences:

README.rst
48 48
 * Beaker >= 1.6:: http://pypi.python.org/pypi/Beaker
49 49
 * Mako >= 0.4:: http://pypi.python.org/pypi/Mako
50 50
 * lxml >= 2.3:: http://pypi.python.org/pypi/lxml
51
 * xtraceback >= 0.3:: http://pypi.python.org/pypi/xtraceback
52
 * sqlalchemy-migrate:: http://pypi.python.org/pypi/sqlalchemy-migrate
53

  
51 54

  
52 55
You can install all those dependencies quickly using pip::
53 56

  
54
   pip install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
57
   pip install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
55 58

  
56 59
or easy_install::
57 60

  
58
   easy_install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
61
   easy_install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
59 62

  
60 63
or apt-get (Debian based distributions)::
61 64

  
mandaye/__init__.py
1
VERSION=0.2
1
VERSION=0.3
mandaye/auth/authform.py
3 3
"""
4 4
import Cookie
5 5
import base64
6
import copy
6 7
import re
7 8
import traceback
8 9
import urllib
......
14 15
from lxml.html import fromstring
15 16
from urlparse import parse_qs
16 17

  
17
from mandaye import config
18
from mandaye import config, VERSION
18 19
from mandaye.db import sql_session
20
from mandaye.exceptions import MandayeException
19 21
from mandaye.models import Site, ExtUser, LocalUser
20 22
from mandaye.log import logger
21 23
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
......
38 40
            'form_url': '/myform',
39 41
            'form_attrs': { 'name': 'form40', },
40 42
            'username_field': 'user',
41
            'password_field': 'pwd'
43
            'password_field': 'pass',
44
            'post_fields': ['birthdate', 'card_number']
42 45
        }
46
        form_url, form_attrs, post_fields and username_field are obligatory
43 47
        site_name: str with the site name
44 48
        """
45 49
        if not form_values.has_key('form_headers'):
46 50
            form_values['form_headers'] = {
47 51
                    'Content-Type': 'application/x-www-form-urlencoded',
48
                    'User-Agent': 'Mozilla/5.0 Mandaye/0.0'
52
                    'User-Agent': 'Mozilla/5.0 Mandaye/%s' % VERSION
49 53
                    }
50 54

  
51 55
        if not form_values.has_key('form_url') or \
52 56
                not form_values.has_key('form_attrs') or \
53
                not form_values.has_key('username_field') or \
54
                not form_values.has_key('password_field'):
57
                not form_values.has_key('post_fields') or \
58
                not form_values.has_key('username_field'):
55 59
            logger.critical("Bad configuration: AuthForm form_values dict must have \
56
this keys: form_url, form_attrs, username_field and password_field")
57
            # TODO: manage Mandaye exceptions
58
            raise BaseException, 'AuthForm bad configuration'
60
this keys: form_url, form_attrs, post_fields and username_field")
61
            raise MandayeException, 'AuthForm bad configuration'
62
        if config.encrypt_secret and not form_values.has_key('password_field'):
63
            logger.critical("Bad configuration: AuthForm form_values dict must have a \
64
a password_field key if you want to encode a password.")
65
            raise MandayeException, 'AuthForm bad configuration'
59 66

  
60 67
        self.form_url = form_values['form_url']
61 68
        self.form_values = form_values
69
        if not self.form_values.has_key('post_fields'):
70
            self.form_values['post_fields'] = []
62 71
        self.site_name = site_name
63 72

  
64
    def _encrypt_pwd(self, pwd):
73
    def _encrypt_pwd(self, post_values):
65 74
        """ This method allows you to encrypt a password
66 75
        To use this feature you muste set encrypt_ext_password to True
67 76
        in your configuration and set a secret in encrypt_secret
68
        pwd: the password you want to encrypt
69
        return None if encryption failed
77
        post_values: containt the post values
78
        return None and modify post_values
70 79
        """
71
        logger.debug("Encrypt password")
72
        enc_pwd = pwd
73 80
        if config.encrypt_secret:
74
            try:
75
                cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
76
                enc_pwd = cipher.encrypt(pwd)
77
                enc_pwd = base64.b64encode(enc_pwd)
78
            except Exception, e:
79
                if config.debug:
80
                    traceback.print_exc()
81
                logger.warning('Password encrypting failed %s' % e)
81
            logger.debug("Encrypt password")
82
            password = post_values[self.form_values['password_field']]
83
            if config.encrypt_secret:
84
                try:
85
                    cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
86
                    password = cipher.encrypt(password)
87
                    password = base64.b64encode(password)
88
                    post_values[self.form_values['password_field']] = password
89
                except Exception, e:
90
                    if config.debug:
91
                        traceback.print_exc()
92
                    logger.warning('Password encrypting failed %s' % e)
93
            else:
94
                logger.warning("You must set a secret to use pwd encryption")
82 95
        else:
83
            logger.warning("You must set a secret to use pwd encryption")
84
        return enc_pwd
96
            logger.warning("You must set a password_field to encode a password")
85 97

  
86
    def _decrypt_pwd(self, enc_pwd):
98
    def _decrypt_pwd(self, post_values):
87 99
        """ This method allows you to dencrypt a password encrypt with
88 100
        _encrypt_pwd method. To use this feature you muste set
89 101
        encrypt_ext_password to True in your configuration and
90 102
        set a secret in encrypt_secret
91
        enc_pwd: your encoded password
92
        return None if encryption failed
103
        post_values: containt the post values
104
        return None and modify post_values
93 105
        """
94
        logger.debug("Decrypt password")
95
        pwd = enc_pwd
96 106
        if config.encrypt_secret:
107
            logger.debug("Decrypt password")
108
            password = post_values[self.form_values['password_field']]
97 109
            try:
98
                cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
99
                pwd = base64.b64decode(enc_pwd)
100
                pwd = cipher.decrypt(pwd)
110
                cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
111
                password = base64.b64decode(password)
112
                password = cipher.decrypt(password)
113
                post_values[self.form_values['password_field']] = password
101 114
            except Exception, e:
102 115
                if config.debug:
103 116
                    traceback.print_exc()
104 117
                logger.warning('Decrypting password failed: %s' % e)
105 118
        else:
106 119
            logger.warning("You must set a secret to use pwd decryption")
107
        return pwd
108 120

  
109
    def replay(self, env, username, password, extra_values={}):
121
    def _get_password(self, post_values):
122
        if self.form_values.has_key('password_field'):
123
            if config.encrypt_ext_password:
124
                return self._encrypt_pwd(
125
                        post_values[self.form_values['password_field']]
126
                        )
127
            return post_values[self.form_values['password_field']]
128
        return None
129

  
130
    def replay(self, env, post_values):
110 131
        """ replay the login / password
111 132
        env: WSGI env with beaker session and the target
112
        extra_values: dict with the field name (key) and the field value (value)
133
        post_values: dict with the field name (key) and the field value (value)
113 134
        """
114 135
        if not "://" in self.form_url:
115 136
            self.form_url = env['target'].geturl() + '/' + self.form_url
......
155 176
                    params[input.name] = input.value
156 177
                else:
157 178
                    params[input.name] = ''
158
        params[self.form_values['username_field']] = username
159
        params[self.form_values['password_field']] = password
160
        for key, value in extra_values.iteritems():
179
        for key, value in post_values.iteritems():
161 180
            params[key] = value
162 181
        params = urllib.urlencode(params)
163 182
        request = HTTPRequest(cookies, headers, "POST", params)
164 183
        return get_response(env, request, action, cj)
165 184

  
166
    def _save_association(self, env, local_login, ext_username, ext_pwd, ext_birthdate=None):
185
    def _save_association(self, env, local_login, post_values):
167 186
        """ save an association in the database
168 187
        env: wsgi environment
169 188
        local_login: the Mandaye login
170
        ext_username: username of the external site (use for the replay)
171
        ext_pwd: password of the external site
172
        ext_birthdate: external birthdate (optional)
189
        post_values: dict with the post values
173 190
        """
191
        ext_username = post_values[self.form_values['username_field']]
192
        if config.encrypt_ext_password:
193
            self._encrypt_pwd(post_values)
174 194
        site = sql_session().query(Site).\
175 195
                filter_by(name=self.site_name).first()
176 196
        if not site:
......
194 214
            logger.info('New association: %s with %s on site %s' % \
195 215
                    (ext_username, local_login, self.site_name))
196 216
        ext_user.login = ext_username
197
        if config.encrypt_ext_password:
198
             ext_pwd = self._encrypt_pwd(ext_pwd)
199
        ext_user.password = ext_pwd
217
        ext_user.post_values = post_values
200 218
        ext_user.local_user = local_user
201 219
        ext_user.last_connection = datetime.now()
202 220
        ext_user.site = site
203
        # TODO: generalize this
204
        if ext_birthdate:
205
            ext_user.birthdate = ext_birthdate
206 221
        sql_session().commit()
207 222
        env['beaker.session']['login'] = local_login
208 223
        env['beaker.session'][self.site_name] = ext_user.id
......
221 236
            qs = parse_qs(env['QUERY_STRING'])
222 237
            for key, value in qs.iteritems():
223 238
                qs[key] = value[0]
224
            if not post.has_key('username') or not post.has_key('password'):
225
                logger.info('Association auth failed: form not correctly filled')
226
                qs['type'] = 'badlogin'
227
                return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
228
            username = post['username'][0]
229
            # TODO: generized this part (use a generic key / value table)
230
            extra_values = {}
231
            if post.has_key('birthdate') and self.form_values.has_key('birthdate_field'):
232
                birthdate_field = self.form_values['birthdate_field']
233
                birthdate =  post['birthdate'][0]
234
                extra_values = {birthdate_field: birthdate}
235
            else:
236
                birthdate = None
237
            response = self.replay(env, username,
238
                    post['password'][0], extra_values)
239
            post_fields = self.form_values['post_fields']
240
            post_values = {}
241
            for field in post_fields:
242
                if not post.has_key(field):
243
                    logger.info('Association auth failed: form not correctly filled')
244
                    qs['type'] = 'badlogin'
245
                    return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
246
                post_values[field] = post[field][0]
247
            response = self.replay(env, post_values)
239 248
            if eval(condition):
240 249
                logger.debug("Replay works: save the association")
241
                self._save_association(env, login, username, post['password'][0], birthdate)
250
                self._save_association(env, login, post_values)
242 251
                if qs.has_key('next_url'):
243 252
                    return _302(qs['next_url'], response.cookies)
244 253
                return response
......
250 259
    def _login_ext_user(self, ext_user, env, condition, values):
251 260
        """ Log in an external user
252 261
        """
253
        if not ext_user.login or not ext_user.password:
262
        if not ext_user.login:
254 263
            return _500(env['PATH_INFO'],
255 264
                    'Invalid values for AuthFormDispatcher.login')
256
        # TODO: generized this condition
257
        extra_values = {}
258
        if ext_user.birthdate and self.form_values.has_key('birthdate_field'):
259
            extra_values = { self.form_values['birthdate_field']: ext_user.birthdate }
265
        post_values = copy.deepcopy(ext_user.post_values)
260 266
        if config.encrypt_ext_password:
261
            pwd = self._decrypt_pwd(ext_user.password)
262
        else:
263
            pwd = ext_user.password
264
        response = self.replay(env, ext_user.login,
265
                pwd, extra_values)
267
            self._decrypt_pwd(post_values)
268
        response = self.replay(env, post_values)
266 269
        if condition and eval(condition):
267 270
            ext_user.last_connection = datetime.now()
268 271
            sql_session().commit()
mandaye/auth/espacefamille.py
10 10

  
11 11
class EspaceFamilleAuth(VincennesAuth):
12 12

  
13
    def replay(self, env, username, password, extra_values={}):
13
    def replay(self, env, post_values):
14 14
        """ This hack a hack method
15 15
        There is a bug in httplib so this method make a manual post
16 16
        """
......
25 25
        except Exception, e:
26 26
            return _500('Espace famille: no cookie JSESSIONID', e)
27 27

  
28
        params = {'codeFamille': username, 'motDePasse': password, 'idSession': fsession}
29
        body = urllib.urlencode(params)
28
        post_values['idSession'] = fsession
29
        body = urllib.urlencode(post_values)
30 30
        headers = "POST %s HTTP/1.1\r\nAccept-Encoding: identity\r\n\
31 31
Content-Length: %d\r\nConnection: close\r\n\
32 32
Accept: text/html; */*\r\nHost: %s\r\n\
mandaye/configs/biblio_vincennes.py
5 5
form_values = {
6 6
        'form_url': '/sezhame/page/connexion-abonne',
7 7
        'form_attrs': { 'id': 'dk-opac15-login-form', },
8
        'post_fields': ['user', 'password'],
8 9
        'username_field': 'user',
9
        'password_field': 'password'
10
        'password_field': 'password',
10 11
}
11 12

  
12 13
auth = VincennesAuth(form_values, 'biblio', 'https://www.vincennes.fr/comptecitoyen/auth')
mandaye/configs/duonet_vincennes.py
9 9
form_values = {
10 10
        'form_url': 'Connect.aspx?key=%s' % duonet_key,
11 11
        'form_attrs': { 'name': 'form1' },
12
        'post_fields': ['txtNomFoyer', 'txtDateNaissance', 'txtCode'],
12 13
        'username_field': 'txtNomFoyer',
13
        'birthdate_field': 'txtDateNaissance',
14
        'password_field': 'txtCode',
14
        'password_field': 'txtCode'
15 15
}
16 16

  
17 17

  
mandaye/configs/famille_vincennes.py
9 9
        'form_action': '%s/login.do' % folder_target,
10 10
        'form_url': '%s/index.do' % folder_target,
11 11
        'form_attrs': { 'action': 'login.do', },
12
        'post_fields': ['codeFamille', 'motDePasse'],
12 13
        'username_field': 'codeFamille',
13 14
        'password_field': 'motDePasse'
14 15
}
mandaye/exceptions.py
6 6
    "Mandaye is somehow improperly configured"
7 7
    pass
8 8

  
9
class MandayeException(Exception):
10
    "Mandaye generic exception"
11
    pass
mandaye/models.py
1 1

  
2
import collections
3
import json
2 4

  
3 5
from datetime import datetime
4 6

  
5 7
from sqlalchemy import Column, Integer, String, DateTime
6 8
from sqlalchemy import ForeignKey
7 9
from sqlalchemy.ext.declarative import declarative_base
8
from sqlalchemy.orm import relationship, backref
9

  
10
from sqlalchemy.ext.mutable import Mutable
11
from sqlalchemy.orm import column_property, relationship, backref
12
from sqlalchemy.types import TypeDecorator, VARCHAR
13

  
14

  
15
class JSONEncodedDict(TypeDecorator):
16
    "Represents an immutable structure as a json-encoded string."
17

  
18
    impl = VARCHAR
19

  
20
    def process_bind_param(self, value, dialect):
21
        if value is not None:
22
            value = json.dumps(value)
23
        return value
24

  
25
    def process_result_value(self, value, dialect):
26
        if value is not None:
27
            value = json.loads(value)
28
        return value
29

  
30
class MutationDict(Mutable, dict):
31

  
32
    @classmethod
33
    def coerce(cls, key, value):
34
        """ Convert plain dictionaries to MutationDict. """
35
        if not isinstance(value, MutationDict):
36
            if isinstance(value, dict):
37
                return MutationDict(value)
38
            # this call will raise ValueError
39
            return Mutable.coerce(key, value)
40
        else:
41
            return value
42

  
43
    def __setitem__(self, key, value):
44
        """ Detect dictionary set events and emit change events. """
45
        dict.__setitem__(self, key, value)
46
        self.changed()
47

  
48
    def __delitem__(self, key):
49
        """ Detect dictionary del events and emit change events. """
50
        dict.__delitem__(self, key)
51
        self.changed()
52

  
53
MutationDict.associate_with(JSONEncodedDict)
10 54
Base = declarative_base()
11 55

  
12 56
class Site(Base):
......
22 66
        return "<Site('%s')>" % (self.name)
23 67

  
24 68
class LocalUser(Base):
25
    """ Mandaye local user
69
    """ Mandaye's user
26 70
    """
27 71
    __tablename__ = 'local_users'
28 72

  
29 73
    id = Column(Integer, primary_key=True)
30
    login = Column(String(150), nullable=True, unique=True)
74
    login = Column(String(150), nullable=False, unique=True)
31 75
    password = Column(String(25), nullable=True)
32
    fullname = Column(String(150), nullable=True)
76
    firstname = Column(String(150), nullable=True)
77
    lastname = Column(String(150), nullable=True)
78
    fullname = column_property(firstname + " " + lastname)
79

  
80
    creation_date = Column(DateTime, default=datetime.now(), nullable=False)
81
    last_connection = Column(DateTime, default=datetime.now())
33 82

  
34 83
    def __init__(self, login=None, password=None, fullname=None):
35 84
        self.login = login
......
37 86
        self.fullname = fullname
38 87

  
39 88
    def __repr__(self):
40
        if self.login:
41
            return "<LocalUser('%s')>" % (self.login)
42
        elif self.token:
43
            return "<LocalUser('%s')>" % (self.token)
44
        return "<LocalUser>"
89
        return "<LocalUser('%d %s')>" % (self.id, self.fullname)
45 90

  
46 91
class ExtUser(Base):
47 92
    """ User of externals applications
......
49 94
    __tablename__ = 'ext_users'
50 95

  
51 96
    id = Column(Integer, primary_key=True)
52
    login = Column(String(80), nullable=True)
53
    password = Column(String(25), nullable=True)
54
    birthdate =  Column(String(15), nullable=True)
97
    login = Column(String(150), nullable=False)
98
    post_values = Column(JSONEncodedDict)
99
    creation_date = Column(DateTime, default=datetime.now(), nullable=False)
55 100
    last_connection = Column(DateTime, default=datetime.now())
56 101

  
57 102
    local_user_id = Column(Integer, ForeignKey('local_users.id'), nullable=False)
......
59 104
    local_user = relationship("LocalUser", backref=backref('ext_users'))
60 105
    site = relationship("Site", backref=backref('users'))
61 106

  
62
    def __init__(self, login=None, password=None):
107
    def __init__(self, login=None, post_values=None):
63 108
        self.login = login
64
        self.password = password
109
        self.post_values = post_values
65 110

  
66 111
    def __repr__(self):
67
        if self.login:
68
            return "<ExtUser('%s')>" % (self.login)
69
        elif self.token:
70
            return "<ExtUser('%s')>" % (self.token)
71
        return "<ExtUser>"
72

  
112
        return "<ExtUser '%d'>" % (self.id)
73 113

  
74 114

  
mandaye/templates/biblio/associate.html
32 32
          <form id="dk-opac15-login-form" method="post" accept-charset="UTF-8" action="${action_url}">
33 33
            <div><div class="form-item">
34 34
                <label for="edit-user">Numéro de carte&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>
35
                <input type="text" class="form-text required" value="" size="19" id="edit-user" name="username" maxlength="128">
35
                <input type="text" class="form-text required" value="" size="19" id="edit-user" name="user" maxlength="128">
36 36
              </div>
37 37
              <div class="form-item">
38 38
                <label for="edit-password">Mot de passe&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>
mandaye/templates/duonet/associate.html
47 47
                          <span id="lblPassword">Nom de famille</span>
48 48
                        </td>
49 49
                        <td align="left">
50
                          <input name="username" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
50
                          <input name="txtNomFoyer" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
51 51
                        </td>
52 52
                      </tr> 
53 53

  
......
57 57
                        </td>
58 58

  
59 59
                        <td align="left">
60
                          <input name="birthdate" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
60
                          <input name="txtDateNaissance" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
61 61
                          <span id="lblPassword1" style="font-size:XX-Small;font-style:italic;">(Ex: 16/06/2008)</span>
62 62
                        </td>
63 63
                      </tr>
......
67 67
                          <span id="lblLogin">Code DuoNET</span>
68 68
                        </td>
69 69
                        <td align="left" style="padding-left:3px">
70
                          <input name="password" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
70
                          <input name="txtCode" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
71 71
                          <span id="lblPassword2" style="font-size:XX-Small;font-style:italic;">(Ex: 1994000001001)</span>
72 72
                        </td>
73 73
                      </tr>
mandaye/templates/famille/associate.html
19 19
        % endif
20 20
        <div class="firstline">
21 21
          <label for="cdfmll" class="first"><b>⇒</b> Code famille</label> : 
22
          <input type="text" id="cdfmll" class="txt" name="username" title="Indiquez votre code famille"><br>
22
          <input type="text" id="cdfmll" class="txt" name="codeFamille" title="Indiquez votre code famille"><br>
23 23
          <label for="mtdpss"><b>⇒</b> Mot de passe</label> :
24
          <input type="password" id="mtdpss" class="txt" name="password" title="Indiquez votre mot de passe"></div>
24
          <input type="password" id="mtdpss" class="txt" name="motDePasse" title="Indiquez votre mot de passe"></div>
25 25
        <input type="submit" class="submit" value="Associer"><br>
26 26
      </div>
27 27
    </form>

Formats disponibles : Unified diff