Project

General

Profile

Download (22.3 KB) Statistics
| Branch: | Tag: | Revision:

root / extra / modules / ezldap_ui.ptl @ e8b79d7d

1
'''
2
Module to provide the kind of interaction needed for a eZ publish website
3
acting as reverse proxy and passing in an HTTP header a pointer to an LDAP
4
entry.
5
'''
6

    
7
# FIXME: try/except
8
import ldap
9
import ldap.modlist
10

    
11
import base64
12
import datetime
13
import random
14
import string
15
import urllib
16
from urlparse import urlparse
17
try:
18
    from hashlib import sha1 as sha
19
except ImportError:
20
    from sha import sha
21

    
22
from quixote import get_publisher, get_request, redirect, get_session
23
from quixote.directory import Directory
24

    
25
from qommon import get_cfg, template, emails
26
from qommon import misc
27
from qommon.admin.emails import EmailsDirectory
28
from qommon.admin.texts import TextsDirectory
29
from qommon.form import *
30
from qommon.ident.password import check_password, make_password
31

    
32
from wcs.formdef import FormDef
33
from wcs.users import User
34

    
35
from myspace import MyspaceDirectory
36
from payments import is_payment_supported
37

    
38
import ezldap
39

    
40
def make_token():
41
    '''Create a token, checking it does not already exist in the ldap base.'''
42
    r = random.SystemRandom()
43
    ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
44
    ldap_conn = ezldap.get_ldap_conn()
45
    while True:
46
        token = ''.join([r.choice(string.letters + string.digits) for x in range(16)])
47
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
48
                filterstr='token=%s' % token)
49
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
50
        if result_data == []:
51
            break
52
    return token
53

    
54
def add_token(token, dn, action, exp_days=3):
55
    ''' Add a token in the LDAP  '''
56
    expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
57
    ldap_conn = ezldap.get_ldap_conn()
58
    # add token into the LDAP
59
    mod_list = [(ldap.MOD_REPLACE, 'token', token),
60
            (ldap.MOD_REPLACE, 'tokenAction', action),
61
            (ldap.MOD_REPLACE, 'tokenExpiration', expdate.strftime('%Y%m%d%H%M%SZ'))]
62
    ldap_conn.modify_s(dn, mod_list)
63
    return token
64

    
65
class EzMyspaceDirectory(MyspaceDirectory):
66

    
67
    def __init__(self):
68
        self._q_exports.extend(['change_email'])
69

    
70
    def _index_buttons [html] (self, form_data):
71
        super(EzMyspaceDirectory, self)._index_buttons(form_data)
72
        '<p class="command"><a href="change_email">%s</a></p>' % _('Change my email')
73
        '<br />'
74
        '<br />'
75
        '<p>'
76
        _('You can delete your account freely from the services portal. '
77
          'This action is irreversible; it will destruct your personal '
78
          'datas and destruct the access to your request history.')
79
        ' <strong><a href="remove">%s</a></strong>.' % _('Delete My Account')
80
        '</p>'
81

    
82
    def _my_profile [html] (self, user_formdef, user):
83
        '<h3 id="my-profile">%s</h3>' % _('My Profile')
84

    
85
        TextsDirectory.get_html_text('top-of-profile')
86

    
87
        if user.form_data:
88
            get_publisher().substitutions.feed(get_request().user)
89
            data = get_publisher().substitutions.get_context_variables()
90
            TextsDirectory.get_html_text('profile-presentation', data)
91
        else:
92
            '<p>%s</p>' % _('Empty profile')
93

    
94
    def submit_password(self, new_password):
95
        userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
96
        mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword)]
97
        ezldap.get_ldap_conn().modify_s(get_session().user, mod_list)
98

    
99
    def change_email [html] (self):
100
        form = Form(enctype = 'multipart/form-data')
101
        form.add(ezldap.EzEmailWidget, 'new_email', title = _('New email'),
102
                required=True, size=30)
103
        form.add_submit('submit', _('Submit Request'))
104
        form.add_submit('cancel', _('Cancel'))
105
        if form.get_submit() == 'cancel':
106
            return redirect('.')
107
        if form.is_submitted() and not form.has_errors():
108
            new_email = form.get_widget('new_email').parse()
109
            self.change_email_submit(new_email)
110
            template.html_top(_('Change email'))
111
            TextsDirectory.get_html_text('change-email-token-sent') % { 'new_email': new_email }
112
        else:
113
            template.html_top(_('Change email'))
114
            TextsDirectory.get_html_text('change-email')
115
            form.render()
116

    
117
    def change_email_submit(self, new_email):
118
        data = {}
119

    
120
        token = make_token()
121
        req = get_request()
122
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
123
                urllib.quote(req.environ.get('SCRIPT_NAME')))
124
        data['change_url'] = base_url + '/token/?token=%s' % token
125
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
126
        data['new_email'] = new_email
127
        data['current_email'] = req.user.email
128

    
129
        # add the token in the LDAP
130
        add_token(token, get_session().user, "change_email;%s" % new_email)
131
        # send mail
132
        emails.custom_ezt_email('change-email-request', data,
133
                data['new_email'], fire_and_forget = True)
134

    
135
    def remove [html] (self):
136
        user = get_request().user
137
        if not user or user.anonymous:
138
            raise errors.AccessUnauthorizedError()
139
        form = Form(enctype = 'multipart/form-data')
140
        form.add_submit('submit', _('Remove my account'))
141
        form.add_submit('cancel', _('Cancel'))
142
        if form.get_submit() == 'cancel':
143
            return redirect('.')
144
        if form.is_submitted() and not form.has_errors():
145
            self.remove_email_submit()
146
            template.html_top(_('Removing Account'))
147
            TextsDirectory.get_html_text('remove-token-sent') % { 'email': user.email }
148
        else:
149
            template.html_top(_('Removing Account'))
150
            TextsDirectory.get_html_text('remove-account')
151
            form.render()
152

    
153
    def remove_email_submit(self):
154
        data = {}
155
        token = make_token()
156
        req = get_request()
157
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
158
                urllib.quote(req.environ.get('SCRIPT_NAME')))
159
        data['remove_url'] = base_url + '/token/?token=%s' % token
160
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
161
        data['email'] = req.user.email
162
        # add the token in the LDAP
163
        add_token(token, get_session().user, "remove")
164
        # send mail
165
        emails.custom_ezt_email('remove-request', data,
166
                data['email'], fire_and_forget = True)
167

    
168
class EzTokenDirectory(Directory):
169
    _q_exports = ['', 'cancel']
170

    
171
    def _q_index (self):
172
        '''token processing'''
173
        token = get_request().form.get('token')
174
        if token:
175
            result_data = self._get_ldap_entry_token(token)
176
            if result_data:
177
                return self._actions(result_data[0][1]['tokenAction'][0], result_data)
178
        template.html_top(_('Error'))
179
        return _('The token you submitted does not exist, has expired, or has been cancelled.')
180

    
181
    def _get_ldap_entry_token(self, token):
182
        ''' return a ldap result for the given token '''
183
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
184
        ldap_conn = ezldap.get_ldap_conn()
185
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
186
                    filterstr='token=%s' % token)
187
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
188
        if result_type == ldap.RES_SEARCH_ENTRY:
189
            return result_data
190
        else:
191
            return None
192

    
193
    def _actions(self, token_action, result_data):
194
        dn = result_data[0][0]
195
        action = token_action.split(";")[0]
196
        if action == 'create':
197
            mod_list = [(ldap.MOD_REPLACE, 'actif', 'TRUE'),
198
                    (ldap.MOD_DELETE, 'token', None),
199
                    (ldap.MOD_DELETE, 'tokenAction', None),
200
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
201
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
202
            template.html_top(_('Welcome'))
203
            return TextsDirectory.get_html_text('account-created')
204
        elif action == 'change_email':
205
            new_email = token_action.split(";")[1]
206
            mod_list = [(ldap.MOD_REPLACE, ezldap.LDAP_EMAIL, str(new_email)),
207
                    (ldap.MOD_DELETE, 'token', None),
208
                    (ldap.MOD_DELETE, 'tokenAction', None),
209
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
210
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
211
            template.html_top(_('Email changed'))
212
            return TextsDirectory.get_html_text('new-email-confirmed') % { 'new_email': new_email }
213
        elif action == 'change_password':
214
            email = result_data[0][1][ezldap.LDAP_EMAIL][0]
215
            new_password = make_password()
216
            userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
217
            mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword),
218
                    (ldap.MOD_DELETE, 'token', None),
219
                    (ldap.MOD_DELETE, 'tokenAction', None),
220
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
221
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
222
            data = {
223
                 'username': email,
224
                 'password': new_password,
225
                 'hostname': get_request().get_server(),
226
            }
227
            emails.custom_ezt_email('new-generated-password', data, email,
228
                    exclude_current_user = False)
229
            template.html_top(_('New password sent by email'))
230
            return TextsDirectory.get_html_text('new-password-sent-by-email')
231
        elif action == 'remove':
232
            template.html_top(_('Removing Account'))
233
            # delete all related forms
234
            formdefs = FormDef.select()
235
            user_forms = []
236
            for formdef in formdefs:
237
                user_forms.extend(formdef.data_class().get_with_indexed_value('user_id', dn))
238
            for formdata in user_forms:
239
                formdata.remove_self()
240
            # delete the user in ldap
241
            ezldap.get_ldap_conn().delete_s(dn)
242
            # FIXME : hack for vincennes only...
243
            return redirect('../../logout')
244

    
245
    def cancel(self):
246
        token = get_request().form.get('token')
247
        if token:
248
            result_data = self._get_ldap_entry_token(token)
249
            if result_data:
250
                dn = result_data[0][0]
251
                mod_list = [(ldap.MOD_DELETE, 'token', None),
252
                        (ldap.MOD_DELETE, 'tokenAction', None),
253
                        (ldap.MOD_DELETE, 'tokenExpiration', None)]
254
                ezldap.get_ldap_conn().modify_s(dn, mod_list)
255
                template.html_top(_('Request cancelled'))
256
                return  _('Your request has been cancelled.')
257

    
258
        template.html_top(_('Error'))
259
        return _('The token you submitted does not exist or has expired')
260

    
261

    
262
class EzRegisterDirectory(Directory):
263
    _q_exports = ['', 'passwordreset']
264

    
265
    def _q_index(self):
266
        form = Form(enctype='multipart/form-data')
267

    
268
        template.html_top(_('Registration'))
269
        head = TextsDirectory.get_html_text('new-account')
270
        form.widgets.append(HtmlWidget(head))
271

    
272
        # build password hint : "at least 4 cars, at most 12"
273
        passwords_cfg = get_cfg('passwords', {})
274
        min_len = passwords_cfg.get('min_length', 0)
275
        max_len = passwords_cfg.get('max_length', 0)
276
        if min_len and max_len:
277
            password_hint = _('At least %d characters, at most %d') % (min_len, max_len)
278
        elif min_len:
279
            password_hint = _('At least %d characters') % min_len
280
        elif max_len:
281
            password_hint = _('At most %d characters') % max_len
282
        else:
283
            password_hint = ''
284

    
285
        # FIXME : get names attributes from user_cfg
286
        form.add(StringWidget, 'nom', title=_('Last Name'),
287
                value=form.get('nom'), required=True, size=50)
288
        form.add(StringWidget, 'prenom', title=_('First Name'),
289
                value=form.get('prenom'), required=True, size=50)
290
        form.add(ezldap.EzEmailWidget, ezldap.LDAP_EMAIL, title=_('Email'),
291
                value=form.get(ezldap.LDAP_EMAIL), required=True, size=30,
292
                hint=_('Remember: this will be your username'))
293
        form.add(PasswordWidget, '__password', title=_('New Password'),
294
                value=form.get('__password'), required=True, size=15,
295
                hint=password_hint)
296
        form.add(PasswordWidget, '__password2', title=_('New Password (confirm)'),
297
                value=form.get('__password2'), required=True, size=15)
298
        form.widgets.append(CaptchaWidget('__captcha'))
299
        form.add_submit('submit', _('Register'))
300
        form.add_submit('cancel', _('Cancel'))
301

    
302
        if form.get_submit() == 'cancel':
303
            return redirect('..')
304

    
305
        if form.is_submitted() and not form.has_errors():
306
            check_password(form, '__password')
307
            password = form.get_widget('__password').parse()
308
            password2 = form.get_widget('__password2').parse()
309
            if password != password2:
310
                form.set_error('__password2', _('Passwords do not match'))
311

    
312
        if form.is_submitted() and not form.has_errors():
313
            self.register_submit(form)
314
            template.html_top(_('Welcome'))
315
            return TextsDirectory.get_html_text('email-sent-confirm-creation')
316

    
317
        return form.render()
318

    
319
    def register_submit(self, form):
320
        data = {}
321
        exp_days = 3
322

    
323
        for widget in form.widgets:
324
            value = widget.parse()
325
            if value:
326
                if widget.name == '__password':
327
                    password = value
328
                    data['userPassword'] = ['{sha}%s' % \
329
                            base64.encodestring(sha(password).digest()).strip()]
330
                elif not widget.name.startswith('__'):
331
                    if widget.name.startswith('date'):
332
                        date = datetime.datetime.strptime(value, misc.date_format())
333
                        data[widget.name] = date.strftime('%Y%m%d%H%M%SZ')
334
                    else:
335
                        data[widget.name] = [value]
336

    
337
        misc_cfg = get_cfg('misc', {})
338
        expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
339
        data['actif'] = 'FALSE'
340
        data['token'] = make_token()
341
        data['tokenAction'] = 'create'
342
        data['tokenExpiration'] = expdate.strftime('%Y%m%d%H%M%SZ')
343
        req = get_request()
344
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
345
                urllib.quote(req.environ.get('SCRIPT_NAME')))
346
        token_url = base_url + '/token/?token=%s' % data['token']
347

    
348
        data['objectClass'] = 'vincennesCitoyen'
349
        mod_list = ldap.modlist.addModlist(data)
350
        ldap_dntemplate = misc_cfg.get('aq-ezldap-dntemplate')
351
        while True:
352
            dn = ldap_dntemplate % random.randint(100000, 5000000)
353
            try:
354
                ezldap.get_ldap_conn().add_s(dn, mod_list)
355
            except ldap.ALREADY_EXISTS:
356
                continue
357
            break
358

    
359
        data = {
360
            'email': data[ezldap.LDAP_EMAIL][0],
361
            'username': data[ezldap.LDAP_EMAIL][0],
362
            'password': password,
363
            'token_url': token_url,
364
            'website': base_url,
365
            'admin_email': '',
366
        }
367
        emails.custom_ezt_email('password-subscription-notification', data,
368
                    data.get('email'), fire_and_forget = True)
369

    
370

    
371
    def passwordreset [html] (self):
372
        form = Form(enctype='multipart/form-data')
373
        form.add(EmailWidget, 'email', title=_('Email'), required=True, size=30)
374
        form.add_submit('change', _('Submit Request'))
375

    
376
        if form.is_submitted() and not form.has_errors():
377
            email = form.get_widget('email').parse()
378
            dn = self.get_dn_by_email(email)
379
            if dn:
380
                self.passwordreset_submit(email, dn)
381
                template.html_top(_('Forgotten Password'))
382
                TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
383
            else:
384
                form.set_error('email', _('There is no user with that email.'))
385
                template.html_top(_('Forgotten Password'))
386
                TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
387
                form.render()
388
        else:
389
            template.html_top(_('Forgotten Password'))
390
            TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
391
            form.render()
392

    
393
    def get_dn_by_email(self, email):
394
        ldap_conn = ezldap.get_ldap_conn()
395
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
396
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
397
                    filterstr='%s=%s' % (ezldap.LDAP_EMAIL, email))
398
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
399
        if result_type == ldap.RES_SEARCH_ENTRY:
400
            return result_data[0][0] # dn
401
        else:
402
            return None
403

    
404
    def passwordreset_submit(self, email, dn):
405
        token = make_token()
406
        # add token into the LDAP
407
        add_token(token, dn, "change_password")
408
        # send mail
409
        req = get_request()
410
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
411
                urllib.quote(req.environ.get('SCRIPT_NAME')))
412
        data = { 'change_url': base_url + '/token/?token=%s' % token,
413
                'cancel_url': base_url + '/token/cancel?token=%s' % token,
414
                # FIXME: set the expiration date here
415
                'time': None }
416
        emails.custom_ezt_email('change-password-request', data,
417
                email, exclude_current_user = False)
418

    
419

    
420
def restore_legacy_root(root_directory):
421
    root_directory.myspace = MyspaceDirectory()
422
    from root import AlternateRegisterDirectory
423
    root_directory.register = AlternateRegisterDirectory()
424

    
425
def try_auth(root_directory):
426
    misc_cfg = get_cfg('misc', {})
427
    ldap_url = misc_cfg.get('aq-ezldap-url')
428

    
429
    if not ldap_url:
430
        # no ldap: restore non monkey patched classes & dir
431
        get_publisher().user_class = User
432
        restore_legacy_root(root_directory)
433
        return
434

    
435
    # activate the "magic user" (legacy+ldap)
436
    get_publisher().user_class = ezldap.EzMagicUser
437

    
438
    # reverse proxy detection
439
    reverse_url = get_request().get_header('X-WCS-ReverseProxy-URL')
440
    if reverse_url:
441
        # gorilla patching : SCRIPT_NAME/get_server/get_scheme
442
        parsed = urlparse(reverse_url)
443
        if parsed.path[-1:] == '/':
444
            get_request().environ['SCRIPT_NAME'] = parsed.path[:-1]
445
        else:
446
            get_request().environ['SCRIPT_NAME'] = parsed.path
447
        get_request().get_server = lambda clean=True: parsed.netloc
448
        get_request().get_scheme = lambda : parsed.scheme
449
    else:
450
        restore_legacy_root(root_directory)
451
        return
452

    
453
    # the request must come from the reverse-proxy IP
454
    ezldap_ip = misc_cfg.get('aq-ezldap-ip', None)
455
    if ezldap_ip and get_request().environ.get('REMOTE_ADDR') != ezldap_ip:
456
        # not a good IP: restore non monkey patched classes & dir
457
        restore_legacy_root(root_directory)
458
        return
459

    
460
    # add ldap register & token (anonymous actions)
461
    root_directory.register = EzRegisterDirectory()
462
    root_directory.token = EzTokenDirectory()
463

    
464
    # does the reverse-proxy provide the user DN ?
465
    user_dn = get_request().get_header('X-Auquotidien-Auth-Dn')
466
    session = get_session()
467
    if user_dn:
468
        # this is a LDAP user: set session, activate special myspace
469
        session.set_user(user_dn)
470
        root_directory.myspace = EzMyspaceDirectory()
471
    else:
472
        # disconnected
473
        session.set_user(None)
474
        root_directory.myspace = MyspaceDirectory()
475

    
476

    
477
EmailsDirectory.register('change-email-request',
478
        N_('Request for email change'),
479
        N_('Available variables: new_email, current_email, change_url, cancel_url'),
480
        category = N_('Identification'),
481
        default_subject = N_('Change email Request'),
482
        default_body = N_('''
483
You have requested to associate your account with the email address : [new_email]
484

    
485
Warning : this email will become your username.
486

    
487
To complete the change, visit the following link:
488
[change_url]
489

    
490
If you are not the person who made this request, or you wish to cancel
491
this request, visit the following link:
492
[cancel_url]
493

    
494
If you cancel the contact email change request, your account will remain with
495
your current email ([current_email]).
496

    
497
If you do nothing, the request will lapse after 3 days.
498
'''))
499

    
500
TextsDirectory.register('change-email',
501
        N_('Text when user want to change his email'),
502
        category = N_('Identification'),
503
        default = N_('''
504
You can change your email address here.
505
'''))
506

    
507
TextsDirectory.register('change-email-token-sent',
508
        N_('Text after user had requested to change his email'),
509
        category = N_('Identification'),
510
        default = N_('''
511
A confirmation email has been sent to your new address (%(new_email)s).
512
Follow the instructions in that email to validate your request.
513
'''))
514

    
515
TextsDirectory.register('new-email-confirmed',
516
        N_('Text when new email confirmed by user'),
517
        category = N_('Identification'),
518
        default = N_('''
519
Your email has been changed to : %(new_email)s.
520
Remember that it's your new username !
521
'''))
522

    
523
TextsDirectory.register('profile-presentation',
524
        N_('Profile presentation'),
525
        hint = N_('variables: user fields varnames'),
526
        default = N_('''<p>
527
<ul>
528
  <li>Email: [user_email]</li>
529
</ul>
530
</p>'''))
531

    
532
EmailsDirectory.register('remove-request',
533
        N_('Delete account request'),
534
        N_('Available variables: email, remove_url, cancel_url'),
535
        category = N_('Identification'),
536
        default_subject = N_('Delete account request'),
537
        default_body = N_('''
538
You have requested to *delete* your account [email]
539

    
540
Warning: this action is irreversible; it will destruct your personal
541
datas and destruct the access to your request history.
542

    
543
To complete the request, visit the following link:
544
[remove_url]
545

    
546
If you are not the person who made this request, or you wish to cancel this
547
request, visit the following link:
548
[cancel_url]
549

    
550
If you do nothing, the request will lapse after 3 days.
551
'''))
552

    
553
TextsDirectory.register('remove-account',
554
        N_('Text when user want to delete his email'),
555
        category = N_('Identification'),
556
        default = N_('Are you really sure you want to remove your account?'))
557

    
558
TextsDirectory.register('remove-token-sent',
559
        N_('Text after user had requested to delete his account'),
560
        category = N_('Identification'),
561
        default = N_('''
562
A confirmation email has been sent to your email address.
563
Follow the instructions in that email to validate your request.
564
'''))
565

    
(13-13/26)