Project

General

Profile

Download (24.3 KB) Statistics
| Branch: | Tag: | Revision:
'''
Module to provide the kind of interaction needed for a eZ publish website
acting as reverse proxy and passing in an HTTP header a pointer to an LDAP
entry.
'''

try:
import ldap
import ldap.modlist
except ImportError:
ldap = None

import base64
import datetime
import random
import string
import urllib
from urlparse import urlparse
try:
from hashlib import sha1 as sha
except ImportError:
from sha import sha

from quixote import get_publisher, get_request, redirect, get_session, get_session_manager
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext

from qommon import get_cfg, template, emails
from qommon import misc
from qommon.admin.emails import EmailsDirectory
from qommon.admin.texts import TextsDirectory
from qommon.form import *
from qommon.ident.password import check_password, make_password
from qommon import errors

from wcs.formdef import FormDef
from wcs.users import User

from myspace import MyspaceDirectory
from payments import is_payment_supported

import ezldap

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

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

def email_to_admins(dn, template):
admins = [x for x in get_publisher().user_class.select() if x.is_admin]
if not admins:
return
admin_emails = [x.email for x in admins if x.email]

user = ezldap.EzLdapUser(dn)
data = {
'hostname': get_request().get_server(),
'username': user.email,
'email_as_username': 'True',
'name': user.display_name,
'email': user.email,
}
get_publisher().substitutions.feed(user)

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


class EzMyspaceDirectory(MyspaceDirectory):

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

def _index_buttons(self, form_data):
r = TemplateIO(html=True)
r += super(EzMyspaceDirectory, self)._index_buttons(form_data)
r += htmltext('<p class="command"><a href="change_email">%s</a></p>') % _('Change my email')
r += htmltext('<br />')
r += htmltext('<br />')
r += htmltext('<p>')
r += _('You can delete your account freely from the services portal. '
'This action is irreversible; it will destruct your personal '
'datas and destruct the access to your request history.')
r += htmltext(' <strong><a href="remove">%s</a></strong>.') % _('Delete My Account')
r += htmltext('</p>')
return r.getvalue()

def _my_profile(self, user_formdef, user):
r = TemplateIO(html=True)
r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')

r += TextsDirectory.get_html_text('top-of-profile')

if user.form_data:
get_publisher().substitutions.feed(get_request().user)
data = get_publisher().substitutions.get_context_variables()
r += TextsDirectory.get_html_text('aq-profile-presentation', data)
else:
r += htmltext('<p>%s</p>') % _('Empty profile')
return r.getvalue()

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

def change_email(self):
form = Form(enctype = 'multipart/form-data')
form.add(ezldap.EzEmailWidget, 'new_email', title = _('New email'),
required=True, size=30)
form.add_submit('submit', _('Submit Request'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
new_email = form.get_widget('new_email').parse()
self.change_email_submit(new_email)
template.html_top(_('Change email'))
return TextsDirectory.get_html_text('aq-change-email-token-sent') % { 'new_email': new_email }
else:
template.html_top(_('Change email'))
r = TemplateIO(html=True)
r += TextsDirectory.get_html_text('aq-change-email')
r += form.render()
return r.getvalue()

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

token = make_token()
req = get_request()
base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
urllib.quote(req.environ.get('SCRIPT_NAME')))
data['change_url'] = base_url + '/token/?token=%s' % token
data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
data['new_email'] = new_email
data['current_email'] = req.user.email

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

def remove(self):
user = get_request().user
if not user or user.anonymous:
raise errors.AccessUnauthorizedError()
form = Form(enctype = 'multipart/form-data')
form.add_submit('submit', _('Remove my account'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
self.remove_email_submit()
template.html_top(_('Removing Account'))
return TextsDirectory.get_html_text('aq-remove-token-sent') % { 'email': user.email }
else:
template.html_top(_('Removing Account'))
r = TemplateIO(html=True)
r += TextsDirectory.get_html_text('aq-remove-account')
r += form.render()
return r.getvalue()

def remove_email_submit(self):
data = {}
token = make_token()
req = get_request()
base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
urllib.quote(req.environ.get('SCRIPT_NAME')))
data['remove_url'] = base_url + '/token/?token=%s' % token
data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
data['email'] = req.user.email
# add the token in the LDAP
add_token(token, get_session().user, "remove")
# send mail
emails.custom_ezt_email('aq-remove-request', data,
data['email'], fire_and_forget = True)

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

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

def _get_ldap_entry_token(self, token):
''' return a ldap result for the given token '''
ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
ldap_conn = ezldap.get_ldap_conn()
ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
filterstr='token=%s' % token)
result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
if result_type == ldap.RES_SEARCH_ENTRY:
return result_data
else:
return None

def _actions(self, token_action, result_data):
dn = result_data[0][0]
action = token_action.split(";")[0]
if action == 'create':
mod_list = [(ldap.MOD_REPLACE, 'actif', 'TRUE'),
(ldap.MOD_DELETE, 'token', None),
(ldap.MOD_DELETE, 'tokenAction', None),
(ldap.MOD_DELETE, 'tokenExpiration', None)]
ezldap.get_ldap_conn().modify_s(dn, mod_list)
email_to_admins(dn, 'new-registration-admin-notification')
template.html_top(_('Welcome'))
return TextsDirectory.get_html_text('account-created')
elif action == 'change_email':
new_email = token_action.split(";")[1]
mod_list = [(ldap.MOD_REPLACE, ezldap.LDAP_EMAIL, str(new_email)),
(ldap.MOD_REPLACE, 'actif', 'TRUE'),
(ldap.MOD_DELETE, 'token', None),
(ldap.MOD_DELETE, 'tokenAction', None),
(ldap.MOD_DELETE, 'tokenExpiration', None)]
ezldap.get_ldap_conn().modify_s(dn, mod_list)
template.html_top(_('Email changed'))
return TextsDirectory.get_html_text('aq-new-email-confirmed') % { 'new_email': new_email }
elif action == 'change_password':
email = result_data[0][1][ezldap.LDAP_EMAIL][0]
new_password = make_password()
userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword),
(ldap.MOD_REPLACE, 'actif', 'TRUE'),
(ldap.MOD_DELETE, 'token', None),
(ldap.MOD_DELETE, 'tokenAction', None),
(ldap.MOD_DELETE, 'tokenExpiration', None)]
ezldap.get_ldap_conn().modify_s(dn, mod_list)
data = {
'username': email,
'password': new_password,
'hostname': get_request().get_server(),
}
emails.custom_ezt_email('new-generated-password', data, email,
exclude_current_user = False)
template.html_top(_('New password sent by email'))
return TextsDirectory.get_html_text('new-password-sent-by-email')
elif action == 'remove':
template.html_top(_('Removing Account'))
# email (just before remove)
email_to_admins(dn, 'notification-of-removed-account')
# delete all related forms
formdefs = FormDef.select()
user_forms = []
for formdef in formdefs:
user_forms.extend(formdef.data_class().get_with_indexed_value('user_id', dn))
for formdata in user_forms:
formdata.remove_self()
# delete the user in ldap
ezldap.get_ldap_conn().delete_s(dn)
# delete session and redirect to login page
get_session_manager().expire_session()
root_url = get_publisher().get_root_url()
return redirect('%slogin' % root_url)

def cancel(self):
token = get_request().form.get('token')
if token:
result_data = self._get_ldap_entry_token(token)
if result_data:
dn = result_data[0][0]
mod_list = [(ldap.MOD_DELETE, 'token', None),
(ldap.MOD_DELETE, 'tokenAction', None),
(ldap.MOD_DELETE, 'tokenExpiration', None)]
ezldap.get_ldap_conn().modify_s(dn, mod_list)
template.html_top(_('Request cancelled'))
return _('Your request has been cancelled.')

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


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

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

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

# build password hint : "at least 4 cars, at most 12"
passwords_cfg = get_cfg('passwords', {})
min_len = passwords_cfg.get('min_length', 0)
max_len = passwords_cfg.get('max_length', 0)
if min_len and max_len:
password_hint = _('At least %(min)d characters, at most %(max)d') % \
{'min': min_len, 'max': max_len }
elif min_len:
password_hint = _('At least %d characters') % min_len
elif max_len:
password_hint = _('At most %d characters') % max_len
else:
password_hint = ''

# get required attributes from User configuration
for f in User.get_formdef().fields:
if f.required:
kwargs = {'required': True, 'value': form.get(f.varname)}
for k in f.extra_attributes:
if hasattr(f, k):
kwargs[k] = getattr(f, k)
f.perform_more_widget_changes(form, kwargs)
form.add(f.widget_class, f.varname, title=f.label, hint=f.hint, **kwargs)
# static required fields
form.add(ezldap.EzEmailWidget, ezldap.LDAP_EMAIL, title=_('Email'),
value=form.get(ezldap.LDAP_EMAIL), required=True, size=30,
hint=_('Remember: this will be your username'))
form.add(PasswordWidget, '__password', title=_('New Password'),
value=form.get('__password'), required=True, size=15,
hint=password_hint)
form.add(PasswordWidget, '__password2', title=_('New Password (confirm)'),
value=form.get('__password2'), required=True, size=15)
form.widgets.append(CaptchaWidget('__captcha'))
form.add_submit('submit', _('Register'))
form.add_submit('cancel', _('Cancel'))

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

if form.is_submitted() and not form.has_errors():
check_password(form, '__password')
password = form.get_widget('__password').parse()
password2 = form.get_widget('__password2').parse()
if password != password2:
form.set_error('__password2', _('Passwords do not match'))

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

return form.render()

def register_submit(self, form):
data = {}
exp_days = 3

for widget in form.widgets:
value = widget.parse()
if value:
if widget.name == '__password':
password = value
data['userPassword'] = ['{sha}%s' % \
base64.encodestring(sha(password).digest()).strip()]
elif not widget.name.startswith('__'):
if widget.name.startswith('date'):
date = datetime.datetime.strptime(value, misc.date_format())
data[widget.name] = date.strftime('%Y%m%d%H%M%SZ')
else:
data[widget.name] = [value]

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

data['objectClass'] = 'vincennesCitoyen'
mod_list = ldap.modlist.addModlist(data)
ldap_dntemplate = misc_cfg.get('aq-ezldap-dntemplate')
while True:
dn = ldap_dntemplate % random.randint(100000, 5000000)
try:
ezldap.get_ldap_conn().add_s(dn, mod_list)
except ldap.ALREADY_EXISTS:
continue
break

data = {
'email': data[ezldap.LDAP_EMAIL][0],
'username': data[ezldap.LDAP_EMAIL][0],
'password': password,
'token_url': token_url,
'website': base_url,
'admin_email': '',
}
emails.custom_ezt_email('password-subscription-notification', data,
data.get('email'), fire_and_forget = True)


def passwordreset(self):
form = Form(enctype='multipart/form-data')
form.add(EmailWidget, 'email', title=_('Email'), required=True, size=30)
form.add_submit('change', _('Submit Request'))

if form.is_submitted() and not form.has_errors():
email = form.get_widget('email').parse()
dn = self.get_dn_by_email(email)
if dn:
self.passwordreset_submit(email, dn)
template.html_top(_('Forgotten Password'))
return TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
else:
form.set_error('email', _('There is no user with that email.'))
template.html_top(_('Forgotten Password'))
r = TemplateIO(html=True)
r += TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
r += form.render()
return r.getvalue()
else:
template.html_top(_('Forgotten Password'))
r = TemplateIO(html=True)
r += TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
r += form.render()
return r.getvalue()

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

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


def restore_legacy_root(root_directory):
root_directory.myspace = MyspaceDirectory()
from root import AlternateRegisterDirectory
root_directory.register = AlternateRegisterDirectory()

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

if (ldap is None) or (not ldap_url) or (not get_publisher().has_site_option('ezldap')):
# no ldap: restore non monkey patched classes & dir
get_publisher().user_class = User
restore_legacy_root(root_directory)
return

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

# reverse proxy detection
reverse_url = get_request().get_header('X-WCS-ReverseProxy-URL')
if reverse_url:
# gorilla patching : SCRIPT_NAME/get_server/get_scheme
parsed = urlparse(reverse_url)
if parsed.path[-1:] == '/':
get_request().environ['SCRIPT_NAME'] = parsed.path[:-1]
else:
get_request().environ['SCRIPT_NAME'] = parsed.path
get_request().get_server = lambda clean=True: parsed.netloc
get_request().get_scheme = lambda : parsed.scheme
else:
restore_legacy_root(root_directory)
return

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

# add ldap register & token (anonymous actions)
root_directory.register = EzRegisterDirectory()
root_directory.token = EzTokenDirectory()

# does the reverse-proxy provide the user DN ?
user_dn = get_request().get_header('X-Auquotidien-Auth-Dn')
session = get_session()
if user_dn:
# this is a LDAP user: set session, activate special myspace
session.set_user(user_dn)
root_directory.myspace = EzMyspaceDirectory()
else:
# disconnected
session.set_user(None)
root_directory.myspace = MyspaceDirectory()


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

Warning : this email will become your username.

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

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

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

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

TextsDirectory.register('aq-change-email',
N_('Text when user want to change his email'),
category = N_('Identification'),
default = N_('''
You can change your email address here.
'''))

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

TextsDirectory.register('aq-new-email-confirmed',
N_('Text when new email confirmed by user'),
category = N_('Identification'),
default = N_('''
Your email has been changed to : %(new_email)s.
Remember that it's your new username !
'''))

TextsDirectory.register('aq-profile-presentation',
N_('Profile presentation'),
hint = N_('variables: user fields varnames'),
default = N_('''<p>
<ul>
<li>Email: [session_user_email]</li>
</ul>
</p>'''))

EmailsDirectory.register('aq-remove-request',
N_('Delete account request'),
N_('Available variables: email, remove_url, cancel_url'),
category = N_('Identification'),
default_subject = N_('Delete account request'),
default_body = N_('''
You have requested to *delete* your account [email]

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

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

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

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

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

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

(19-19/33)