Project

General

Profile

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

root / extra / modules / ezldap_ui.ptl @ fea31118

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
def email_to_admins(dn, template):
66
    admins = [x for x in get_publisher().user_class.select() if x.is_admin]
67
    if not admins:
68
        return
69
    admin_emails = [x.email for x in admins if x.email]
70

    
71
    user = ezldap.EzLdapUser(dn)
72
    data = {
73
        'hostname': get_request().get_server(),
74
        'username': user.email,
75
        'email_as_username': 'True',
76
        'name': user.display_name,
77
        'email': user.email,
78
    }
79
    get_publisher().substitutions.feed(user)
80

    
81
    emails.custom_ezt_email(template, data,
82
            admin_emails, fire_and_forget = True)
83

    
84

    
85
class EzMyspaceDirectory(MyspaceDirectory):
86

    
87
    def __init__(self):
88
        self._q_exports.extend(['change_email'])
89

    
90
    def _index_buttons [html] (self, form_data):
91
        super(EzMyspaceDirectory, self)._index_buttons(form_data)
92
        '<p class="command"><a href="change_email">%s</a></p>' % _('Change my email')
93
        '<br />'
94
        '<br />'
95
        '<p>'
96
        _('You can delete your account freely from the services portal. '
97
          'This action is irreversible; it will destruct your personal '
98
          'datas and destruct the access to your request history.')
99
        ' <strong><a href="remove">%s</a></strong>.' % _('Delete My Account')
100
        '</p>'
101

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

    
105
        TextsDirectory.get_html_text('top-of-profile')
106

    
107
        if user.form_data:
108
            get_publisher().substitutions.feed(get_request().user)
109
            data = get_publisher().substitutions.get_context_variables()
110
            TextsDirectory.get_html_text('profile-presentation', data)
111
        else:
112
            '<p>%s</p>' % _('Empty profile')
113

    
114
    def submit_password(self, new_password):
115
        userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
116
        mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword)]
117
        ezldap.get_ldap_conn().modify_s(get_session().user, mod_list)
118

    
119
    def change_email [html] (self):
120
        form = Form(enctype = 'multipart/form-data')
121
        form.add(ezldap.EzEmailWidget, 'new_email', title = _('New email'),
122
                required=True, size=30)
123
        form.add_submit('submit', _('Submit Request'))
124
        form.add_submit('cancel', _('Cancel'))
125
        if form.get_submit() == 'cancel':
126
            return redirect('.')
127
        if form.is_submitted() and not form.has_errors():
128
            new_email = form.get_widget('new_email').parse()
129
            self.change_email_submit(new_email)
130
            template.html_top(_('Change email'))
131
            TextsDirectory.get_html_text('change-email-token-sent') % { 'new_email': new_email }
132
        else:
133
            template.html_top(_('Change email'))
134
            TextsDirectory.get_html_text('change-email')
135
            form.render()
136

    
137
    def change_email_submit(self, new_email):
138
        data = {}
139

    
140
        token = make_token()
141
        req = get_request()
142
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
143
                urllib.quote(req.environ.get('SCRIPT_NAME')))
144
        data['change_url'] = base_url + '/token/?token=%s' % token
145
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
146
        data['new_email'] = new_email
147
        data['current_email'] = req.user.email
148

    
149
        # add the token in the LDAP
150
        add_token(token, get_session().user, "change_email;%s" % new_email)
151
        # send mail
152
        emails.custom_ezt_email('change-email-request', data,
153
                data['new_email'], fire_and_forget = True)
154

    
155
    def remove [html] (self):
156
        user = get_request().user
157
        if not user or user.anonymous:
158
            raise errors.AccessUnauthorizedError()
159
        form = Form(enctype = 'multipart/form-data')
160
        form.add_submit('submit', _('Remove my account'))
161
        form.add_submit('cancel', _('Cancel'))
162
        if form.get_submit() == 'cancel':
163
            return redirect('.')
164
        if form.is_submitted() and not form.has_errors():
165
            self.remove_email_submit()
166
            template.html_top(_('Removing Account'))
167
            TextsDirectory.get_html_text('remove-token-sent') % { 'email': user.email }
168
        else:
169
            template.html_top(_('Removing Account'))
170
            TextsDirectory.get_html_text('remove-account')
171
            form.render()
172

    
173
    def remove_email_submit(self):
174
        data = {}
175
        token = make_token()
176
        req = get_request()
177
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
178
                urllib.quote(req.environ.get('SCRIPT_NAME')))
179
        data['remove_url'] = base_url + '/token/?token=%s' % token
180
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
181
        data['email'] = req.user.email
182
        # add the token in the LDAP
183
        add_token(token, get_session().user, "remove")
184
        # send mail
185
        emails.custom_ezt_email('remove-request', data,
186
                data['email'], fire_and_forget = True)
187

    
188
class EzTokenDirectory(Directory):
189
    _q_exports = ['', 'cancel']
190

    
191
    def _q_index (self):
192
        '''token processing'''
193
        token = get_request().form.get('token')
194
        if token:
195
            result_data = self._get_ldap_entry_token(token)
196
            if result_data:
197
                return self._actions(result_data[0][1]['tokenAction'][0], result_data)
198
        template.html_top(_('Error'))
199
        return _('The token you submitted does not exist, has expired, or has been cancelled.')
200

    
201
    def _get_ldap_entry_token(self, token):
202
        ''' return a ldap result for the given token '''
203
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
204
        ldap_conn = ezldap.get_ldap_conn()
205
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
206
                    filterstr='token=%s' % token)
207
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
208
        if result_type == ldap.RES_SEARCH_ENTRY:
209
            return result_data
210
        else:
211
            return None
212

    
213
    def _actions(self, token_action, result_data):
214
        dn = result_data[0][0]
215
        action = token_action.split(";")[0]
216
        if action == 'create':
217
            mod_list = [(ldap.MOD_REPLACE, 'actif', 'TRUE'),
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
            email_to_admins(dn, 'new-registration-admin-notification')
223
            template.html_top(_('Welcome'))
224
            return TextsDirectory.get_html_text('account-created')
225
        elif action == 'change_email':
226
            new_email = token_action.split(";")[1]
227
            mod_list = [(ldap.MOD_REPLACE, ezldap.LDAP_EMAIL, str(new_email)),
228
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
229
                    (ldap.MOD_DELETE, 'token', None),
230
                    (ldap.MOD_DELETE, 'tokenAction', None),
231
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
232
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
233
            template.html_top(_('Email changed'))
234
            return TextsDirectory.get_html_text('new-email-confirmed') % { 'new_email': new_email }
235
        elif action == 'change_password':
236
            email = result_data[0][1][ezldap.LDAP_EMAIL][0]
237
            new_password = make_password()
238
            userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
239
            mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword),
240
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
241
                    (ldap.MOD_DELETE, 'token', None),
242
                    (ldap.MOD_DELETE, 'tokenAction', None),
243
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
244
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
245
            data = {
246
                 'username': email,
247
                 'password': new_password,
248
                 'hostname': get_request().get_server(),
249
            }
250
            emails.custom_ezt_email('new-generated-password', data, email,
251
                    exclude_current_user = False)
252
            template.html_top(_('New password sent by email'))
253
            return TextsDirectory.get_html_text('new-password-sent-by-email')
254
        elif action == 'remove':
255
            template.html_top(_('Removing Account'))
256
            # delete all related forms
257
            formdefs = FormDef.select()
258
            user_forms = []
259
            for formdef in formdefs:
260
                user_forms.extend(formdef.data_class().get_with_indexed_value('user_id', dn))
261
            for formdata in user_forms:
262
                formdata.remove_self()
263
            # delete the user in ldap
264
            ezldap.get_ldap_conn().delete_s(dn)
265
            # FIXME : hack for vincennes only...
266
            return redirect('../../logout')
267

    
268
    def cancel(self):
269
        token = get_request().form.get('token')
270
        if token:
271
            result_data = self._get_ldap_entry_token(token)
272
            if result_data:
273
                dn = result_data[0][0]
274
                mod_list = [(ldap.MOD_DELETE, 'token', None),
275
                        (ldap.MOD_DELETE, 'tokenAction', None),
276
                        (ldap.MOD_DELETE, 'tokenExpiration', None)]
277
                ezldap.get_ldap_conn().modify_s(dn, mod_list)
278
                template.html_top(_('Request cancelled'))
279
                return  _('Your request has been cancelled.')
280

    
281
        template.html_top(_('Error'))
282
        return _('The token you submitted does not exist or has expired')
283

    
284

    
285
class EzRegisterDirectory(Directory):
286
    _q_exports = ['', 'passwordreset']
287

    
288
    def _q_index(self):
289
        form = Form(enctype='multipart/form-data')
290

    
291
        template.html_top(_('Registration'))
292
        head = TextsDirectory.get_html_text('new-account')
293
        form.widgets.append(HtmlWidget(head))
294

    
295
        # build password hint : "at least 4 cars, at most 12"
296
        passwords_cfg = get_cfg('passwords', {})
297
        min_len = passwords_cfg.get('min_length', 0)
298
        max_len = passwords_cfg.get('max_length', 0)
299
        if min_len and max_len:
300
            password_hint = _('At least %d characters, at most %d') % (min_len, max_len)
301
        elif min_len:
302
            password_hint = _('At least %d characters') % min_len
303
        elif max_len:
304
            password_hint = _('At most %d characters') % max_len
305
        else:
306
            password_hint = ''
307

    
308
        # FIXME : get names attributes from user_cfg
309
        form.add(StringWidget, 'nom', title=_('Last Name'),
310
                value=form.get('nom'), required=True, size=50)
311
        form.add(StringWidget, 'prenom', title=_('First Name'),
312
                value=form.get('prenom'), required=True, size=50)
313
        form.add(ezldap.EzEmailWidget, ezldap.LDAP_EMAIL, title=_('Email'),
314
                value=form.get(ezldap.LDAP_EMAIL), required=True, size=30,
315
                hint=_('Remember: this will be your username'))
316
        form.add(PasswordWidget, '__password', title=_('New Password'),
317
                value=form.get('__password'), required=True, size=15,
318
                hint=password_hint)
319
        form.add(PasswordWidget, '__password2', title=_('New Password (confirm)'),
320
                value=form.get('__password2'), required=True, size=15)
321
        form.widgets.append(CaptchaWidget('__captcha'))
322
        form.add_submit('submit', _('Register'))
323
        form.add_submit('cancel', _('Cancel'))
324

    
325
        if form.get_submit() == 'cancel':
326
            return redirect('..')
327

    
328
        if form.is_submitted() and not form.has_errors():
329
            check_password(form, '__password')
330
            password = form.get_widget('__password').parse()
331
            password2 = form.get_widget('__password2').parse()
332
            if password != password2:
333
                form.set_error('__password2', _('Passwords do not match'))
334

    
335
        if form.is_submitted() and not form.has_errors():
336
            self.register_submit(form)
337
            template.html_top(_('Welcome'))
338
            return TextsDirectory.get_html_text('email-sent-confirm-creation')
339

    
340
        return form.render()
341

    
342
    def register_submit(self, form):
343
        data = {}
344
        exp_days = 3
345

    
346
        for widget in form.widgets:
347
            value = widget.parse()
348
            if value:
349
                if widget.name == '__password':
350
                    password = value
351
                    data['userPassword'] = ['{sha}%s' % \
352
                            base64.encodestring(sha(password).digest()).strip()]
353
                elif not widget.name.startswith('__'):
354
                    if widget.name.startswith('date'):
355
                        date = datetime.datetime.strptime(value, misc.date_format())
356
                        data[widget.name] = date.strftime('%Y%m%d%H%M%SZ')
357
                    else:
358
                        data[widget.name] = [value]
359

    
360
        misc_cfg = get_cfg('misc', {})
361
        expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
362
        data['actif'] = 'FALSE'
363
        data['token'] = make_token()
364
        data['tokenAction'] = 'create'
365
        data['tokenExpiration'] = expdate.strftime('%Y%m%d%H%M%SZ')
366
        req = get_request()
367
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
368
                urllib.quote(req.environ.get('SCRIPT_NAME')))
369
        token_url = base_url + '/token/?token=%s' % data['token']
370

    
371
        data['objectClass'] = 'vincennesCitoyen'
372
        mod_list = ldap.modlist.addModlist(data)
373
        ldap_dntemplate = misc_cfg.get('aq-ezldap-dntemplate')
374
        while True:
375
            dn = ldap_dntemplate % random.randint(100000, 5000000)
376
            try:
377
                ezldap.get_ldap_conn().add_s(dn, mod_list)
378
            except ldap.ALREADY_EXISTS:
379
                continue
380
            break
381

    
382
        data = {
383
            'email': data[ezldap.LDAP_EMAIL][0],
384
            'username': data[ezldap.LDAP_EMAIL][0],
385
            'password': password,
386
            'token_url': token_url,
387
            'website': base_url,
388
            'admin_email': '',
389
        }
390
        emails.custom_ezt_email('password-subscription-notification', data,
391
                    data.get('email'), fire_and_forget = True)
392

    
393

    
394
    def passwordreset [html] (self):
395
        form = Form(enctype='multipart/form-data')
396
        form.add(EmailWidget, 'email', title=_('Email'), required=True, size=30)
397
        form.add_submit('change', _('Submit Request'))
398

    
399
        if form.is_submitted() and not form.has_errors():
400
            email = form.get_widget('email').parse()
401
            dn = self.get_dn_by_email(email)
402
            if dn:
403
                self.passwordreset_submit(email, dn)
404
                template.html_top(_('Forgotten Password'))
405
                TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
406
            else:
407
                form.set_error('email', _('There is no user with that email.'))
408
                template.html_top(_('Forgotten Password'))
409
                TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
410
                form.render()
411
        else:
412
            template.html_top(_('Forgotten Password'))
413
            TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
414
            form.render()
415

    
416
    def get_dn_by_email(self, email):
417
        ldap_conn = ezldap.get_ldap_conn()
418
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
419
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
420
                    filterstr='%s=%s' % (ezldap.LDAP_EMAIL, email))
421
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
422
        if result_type == ldap.RES_SEARCH_ENTRY:
423
            return result_data[0][0] # dn
424
        else:
425
            return None
426

    
427
    def passwordreset_submit(self, email, dn):
428
        token = make_token()
429
        # add token into the LDAP
430
        add_token(token, dn, "change_password")
431
        # send mail
432
        req = get_request()
433
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
434
                urllib.quote(req.environ.get('SCRIPT_NAME')))
435
        data = { 'change_url': base_url + '/token/?token=%s' % token,
436
                'cancel_url': base_url + '/token/cancel?token=%s' % token,
437
                # FIXME: set the expiration date here
438
                'time': None }
439
        emails.custom_ezt_email('change-password-request', data,
440
                email, exclude_current_user = False)
441

    
442

    
443
def restore_legacy_root(root_directory):
444
    root_directory.myspace = MyspaceDirectory()
445
    from root import AlternateRegisterDirectory
446
    root_directory.register = AlternateRegisterDirectory()
447

    
448
def try_auth(root_directory):
449
    misc_cfg = get_cfg('misc', {})
450
    ldap_url = misc_cfg.get('aq-ezldap-url')
451

    
452
    if not ldap_url:
453
        # no ldap: restore non monkey patched classes & dir
454
        get_publisher().user_class = User
455
        restore_legacy_root(root_directory)
456
        return
457

    
458
    # activate the "magic user" (legacy+ldap)
459
    get_publisher().user_class = ezldap.EzMagicUser
460

    
461
    # reverse proxy detection
462
    reverse_url = get_request().get_header('X-WCS-ReverseProxy-URL')
463
    if reverse_url:
464
        # gorilla patching : SCRIPT_NAME/get_server/get_scheme
465
        parsed = urlparse(reverse_url)
466
        if parsed.path[-1:] == '/':
467
            get_request().environ['SCRIPT_NAME'] = parsed.path[:-1]
468
        else:
469
            get_request().environ['SCRIPT_NAME'] = parsed.path
470
        get_request().get_server = lambda clean=True: parsed.netloc
471
        get_request().get_scheme = lambda : parsed.scheme
472
    else:
473
        restore_legacy_root(root_directory)
474
        return
475

    
476
    # the request must come from the reverse-proxy IP
477
    ezldap_ip = misc_cfg.get('aq-ezldap-ip', None)
478
    if ezldap_ip and get_request().environ.get('REMOTE_ADDR') != ezldap_ip:
479
        # not a good IP: restore non monkey patched classes & dir
480
        restore_legacy_root(root_directory)
481
        return
482

    
483
    # add ldap register & token (anonymous actions)
484
    root_directory.register = EzRegisterDirectory()
485
    root_directory.token = EzTokenDirectory()
486

    
487
    # does the reverse-proxy provide the user DN ?
488
    user_dn = get_request().get_header('X-Auquotidien-Auth-Dn')
489
    session = get_session()
490
    if user_dn:
491
        # this is a LDAP user: set session, activate special myspace
492
        session.set_user(user_dn)
493
        root_directory.myspace = EzMyspaceDirectory()
494
    else:
495
        # disconnected
496
        session.set_user(None)
497
        root_directory.myspace = MyspaceDirectory()
498

    
499

    
500
EmailsDirectory.register('change-email-request',
501
        N_('Request for email change'),
502
        N_('Available variables: new_email, current_email, change_url, cancel_url'),
503
        category = N_('Identification'),
504
        default_subject = N_('Change email Request'),
505
        default_body = N_('''
506
You have requested to associate your account with the email address : [new_email]
507

    
508
Warning : this email will become your username.
509

    
510
To complete the change, visit the following link:
511
[change_url]
512

    
513
If you are not the person who made this request, or you wish to cancel
514
this request, visit the following link:
515
[cancel_url]
516

    
517
If you cancel the contact email change request, your account will remain with
518
your current email ([current_email]).
519

    
520
If you do nothing, the request will lapse after 3 days.
521
'''))
522

    
523
TextsDirectory.register('change-email',
524
        N_('Text when user want to change his email'),
525
        category = N_('Identification'),
526
        default = N_('''
527
You can change your email address here.
528
'''))
529

    
530
TextsDirectory.register('change-email-token-sent',
531
        N_('Text after user had requested to change his email'),
532
        category = N_('Identification'),
533
        default = N_('''
534
A confirmation email has been sent to your new address (%(new_email)s).
535
Follow the instructions in that email to validate your request.
536
'''))
537

    
538
TextsDirectory.register('new-email-confirmed',
539
        N_('Text when new email confirmed by user'),
540
        category = N_('Identification'),
541
        default = N_('''
542
Your email has been changed to : %(new_email)s.
543
Remember that it's your new username !
544
'''))
545

    
546
TextsDirectory.register('profile-presentation',
547
        N_('Profile presentation'),
548
        hint = N_('variables: user fields varnames'),
549
        default = N_('''<p>
550
<ul>
551
  <li>Email: [user_email]</li>
552
</ul>
553
</p>'''))
554

    
555
EmailsDirectory.register('remove-request',
556
        N_('Delete account request'),
557
        N_('Available variables: email, remove_url, cancel_url'),
558
        category = N_('Identification'),
559
        default_subject = N_('Delete account request'),
560
        default_body = N_('''
561
You have requested to *delete* your account [email]
562

    
563
Warning: this action is irreversible; it will destruct your personal
564
datas and destruct the access to your request history.
565

    
566
To complete the request, visit the following link:
567
[remove_url]
568

    
569
If you are not the person who made this request, or you wish to cancel this
570
request, visit the following link:
571
[cancel_url]
572

    
573
If you do nothing, the request will lapse after 3 days.
574
'''))
575

    
576
TextsDirectory.register('remove-account',
577
        N_('Text when user want to delete his email'),
578
        category = N_('Identification'),
579
        default = N_('Are you really sure you want to remove your account?'))
580

    
581
TextsDirectory.register('remove-token-sent',
582
        N_('Text after user had requested to delete his account'),
583
        category = N_('Identification'),
584
        default = N_('''
585
A confirmation email has been sent to your email address.
586
Follow the instructions in that email to validate your request.
587
'''))
588

    
(13-13/26)