PasswordTool.py 12 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
##############################################################################
#
# Copyright (c) 2008 Nexedi SARL and Contributors. All Rights Reserved.
#                    Aurelien Calonne <aurel@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

30
import socket
31 32

from AccessControl import ClassSecurityInfo
33
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, get_request
34 35 36
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions
from Products.ERP5 import _dtmldir
37
from zLOG import LOG, INFO
38
import time, random
39
from hashlib import md5
40
from DateTime import DateTime
41
from Products.ERP5Type.Message import translateString
42
from Products.ERP5Type.Globals import PersistentMapping
43
from BTrees.OOBTree import OOBTree
44
from urllib import urlencode
45

46 47 48 49 50 51 52 53 54
redirect_path = '/login_form'
def redirect(REQUEST, site_url, message):
  if REQUEST is not None and getattr(REQUEST.RESPONSE, 'redirect', None) is not None:
    parameter = urlencode({'portal_status_message': message})
    ret_url = '%s%s?%s' % (site_url, redirect_path, parameter)
    return REQUEST.RESPONSE.redirect( ret_url )
  else:
    return message

55 56
class PasswordTool(BaseTool):
  """
Jérome Perrin's avatar
Jérome Perrin committed
57
    PasswordTool is used to allow a user to change its password
58 59 60 61 62 63 64 65 66
  """
  title = 'Password Tool'
  id = 'portal_password'
  meta_type = 'ERP5 Password Tool'
  portal_type = 'Password Tool'
  allowed_types = ()

  # Declarative Security
  security = ClassSecurityInfo()
Vincent Pelletier's avatar
Vincent Pelletier committed
67 68
  security.declareProtected(Permissions.ManagePortal, 'manage_overview')
  manage_overview = DTMLFile('explainPasswordTool', _dtmldir)
69 70

  _expiration_day = 1
71

72
  def __init__(self, id=None):
73
    super(PasswordTool, self).__init__(id)
74
    self._password_request_dict = OOBTree()
75

Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
76
  security.declareProtected('Manage users', 'getResetPasswordKey')
77 78 79 80
  def getResetPasswordKey(self, user_login, expiration_date=None):
    if expiration_date is None:
      # generate expiration date
      expiration_date = DateTime() + self._expiration_day
81 82

    # generate a random string
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
83
    key = self._generateUUID()
84 85 86 87
    if isinstance(self._password_request_dict, PersistentMapping):
      LOG('ERP5.PasswordTool', INFO, 'Migrating password_request_dict to'
                                     ' OOBTree')
      self._password_request_dict = OOBTree(self._password_request_dict)
88 89

    # register request
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
90 91 92 93
    self._password_request_dict[key] = (user_login, expiration_date)
    return key

  security.declareProtected('Manage users', 'getResetPasswordUrl')
94
  def getResetPasswordUrl(self, user_login=None, key=None, site_url=None):
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
95 96 97 98
    if user_login is not None:
      # XXX Backward compatibility
      key = self.getResetPasswordKey(user_login)

99 100 101 102 103 104 105 106
    parameter = urlencode(dict(reset_key=key))
    method = self._getTypeBasedMethod("getSiteUrl")
    if method is not None:
      base_url = method()
    else:
      base_url = "%s/portal_password/PasswordTool_viewResetPassword" % (
        site_url,)
    url = "%s?%s" %(base_url, parameter)
107 108
    return url

109
  def _getExpirationDateForKey(self, key=None):
110 111
    return self._password_request_dict[key][1]

112
  security.declarePublic('mailPasswordResetRequest')
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
113
  def mailPasswordResetRequest(self, user_login=None, REQUEST=None,
114 115
                               notification_message=None, sender=None,
                               store_as_event=False,
116
                               expiration_date=None,
117 118
                               substitution_method_parameter_dict=None,
                               batch=False):
119
    """
Jérome Perrin's avatar
Jérome Perrin committed
120
    Create a random string and expiration date for request
121 122 123
    Parameters:
    user_login -- Reference of the user to send password reset link
    REQUEST -- Request object
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
124
    notification_message -- Notification Message Document used to build the email.
Jérome Perrin's avatar
typos  
Jérome Perrin committed
125
                            As default, a standard text will be used.
126
    sender -- Sender (Person or Organisation) of the email.
127 128 129
            As default, the default email address will be used
    store_as_event -- whenever CRM is available, store
                        notifications as events
130
    expiration_date -- If not set, expiration date is current date + 1 day.
131 132
    substitution_method_parameter_dict -- additional substitution dict for
                                          creating an email.
133
    """
134 135 136
    if REQUEST is None:
      REQUEST = get_request()

137 138 139
    if user_login is None:
      user_login = REQUEST["user_login"]

140 141 142 143
    site_url = self.getPortalObject().absolute_url()
    if REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from

144 145
    msg = None
    # check user exists, and have an email
146
    user_path_set = {x['path'] for x in self.getPortalObject().acl_users.searchUsers(
Vincent Pelletier's avatar
Vincent Pelletier committed
147 148
      login=user_login,
      exact_match=True,
149 150
    ) if 'path' in x}
    if len(user_path_set) == 0:
Yusei Tahara's avatar
Yusei Tahara committed
151
      msg = translateString("User ${user} does not exist.",
152
                            mapping={'user':user_login})
153 154 155
    else:
      # We use checked_permission to prevent errors when trying to acquire
      # email from organisation
156
      user_path, = user_path_set
Vincent Pelletier's avatar
Vincent Pelletier committed
157
      user_value = self.getPortalObject().unrestrictedTraverse(
158
        user_path)
Vincent Pelletier's avatar
Vincent Pelletier committed
159 160
      email_value = user_value.getDefaultEmailValue(
        checked_permission='Access content information')
161 162 163 164 165
      if email_value is None or not email_value.asText():
        msg = translateString(
            "User ${user} does not have an email address, please contact site "
            "administrator directly", mapping={'user':user_login})
    if msg:
166 167 168 169
      if batch:
        raise RuntimeError(msg)
      else:
        return redirect(REQUEST, site_url, msg)
170

171 172
    key = self.getResetPasswordKey(user_login=user_login,
                                   expiration_date=expiration_date)
Aurel's avatar
Aurel committed
173
    url = self.getResetPasswordUrl(key=key, site_url=site_url)
174 175

    # send mail
176 177
    message_dict = {'instance_name':self.getPortalObject().getTitle(),
                    'reset_password_link':url,
178
                    'expiration_date':self._getExpirationDateForKey(key)}
179 180
    if substitution_method_parameter_dict is not None:
      message_dict.update(substitution_method_parameter_dict)
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196

    if notification_message is None:
      subject = translateString("[${instance_name}] Reset of your password",
          mapping={'instance_name': self.getPortalObject().getTitle()})
      subject = subject.translate()
      message = translateString("\nYou requested to reset your ${instance_name}"\
                " account password.\n\n" \
                "Please copy and paste the following link into your browser: \n"\
                "${reset_password_link}\n\n" \
                "Please note that this link will be valid only one time, until "\
                "${expiration_date}.\n" \
                "After this date, or after having used this link, you will have to make " \
                "a new request\n\n" \
                "Thank you",
                mapping=message_dict)
      message = message.translate()
197
      event_keyword_argument_dict={}
198
      message_text_format = 'text/plain'
199
    else:
200
      message_text_format = notification_message.getContentType()
201
      subject = notification_message.getTitle()
202
      if message_text_format == "text/html":
203 204 205
        message = notification_message.asEntireHTML(substitution_method_parameter_dict=message_dict)
      else:
        message = notification_message.asText(substitution_method_parameter_dict=message_dict)
206 207 208 209
      event_keyword_argument_dict={
        'resource':notification_message.getSpecialise(),
        'language':notification_message.getLanguage(),
      }
210

Vincent Pelletier's avatar
Vincent Pelletier committed
211
    self.getPortalObject().portal_notifications.sendMessage(sender=sender, recipient=[user_value,],
212
                                                            subject=subject, message=message,
213
                                                            store_as_event=store_as_event,
214
                                                            message_text_format=message_text_format,
215
                                                            event_keyword_argument_dict=event_keyword_argument_dict)
216 217 218
    if not batch:
      return redirect(REQUEST, site_url,
                      translateString("An email has been sent to you."))
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

  def _generateUUID(self, args=""):
    """
    Generate a unique id that will be used as url for password
    """
    # this code is based on
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/213761
    # by Carl Free Jr
    # as uuid module is only available in pyhton 2.5
    t = long( time.time() * 1000 )
    r = long( random.random()*100000000000000000L )
    try:
      a = socket.gethostbyname( socket.gethostname() )
    except:
      # if we can't get a network address, just imagine one
      a = random.random()*100000000000000000L
235
    data = ' '.join((str(t), str(r), str(a), str(args)))
236
    return md5(data).hexdigest()
237

238
  security.declareProtected(Permissions.ModifyPortalContent, 'removeExpiredRequests')
Vincent Pelletier's avatar
Vincent Pelletier committed
239
  def removeExpiredRequests(self):
240 241 242 243
    """
    Browse dict and remove expired request
    """
    current_date = DateTime()
Vincent Pelletier's avatar
Vincent Pelletier committed
244 245
    password_request_dict = self._password_request_dict
    for key, (_, date) in password_request_dict.items():
246
      if date < current_date:
Vincent Pelletier's avatar
Vincent Pelletier committed
247
        del password_request_dict[key]
248

249
  security.declarePublic('changeUserPassword')
Aurel's avatar
Aurel committed
250 251
  def changeUserPassword(self, password, password_key, password_confirm=None,
                         user_login=None, REQUEST=None, **kw):
252
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
253
    Reset the password for a given login
254
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
255 256 257 258 259 260
    # BBB: password_confirm: unused argument
    def error(message):
      # BBB: should "raise Redirect" instead of just returning, simplifying
      #      calling code and making mistakes more difficult
      # BBB: should probably not translate message when REQUEST is None
      message = translateString(message)
261
      return redirect(REQUEST, site_url, message)
262

263 264 265 266
    if REQUEST is None:
      REQUEST = get_request()
    if self.getWebSiteValue():
      site_url = self.getWebSiteValue().absolute_url()
Vincent Pelletier's avatar
Vincent Pelletier committed
267 268 269 270 271 272 273
    elif REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from
    else:
      site_url = self.getPortalObject().absolute_url()
    try:
      register_user_login, expiration_date = self._password_request_dict[
        password_key]
274
    except (KeyError, TypeError):
Vincent Pelletier's avatar
Vincent Pelletier committed
275 276 277 278 279 280 281 282 283
      # XXX: incorrect grammar and not descriptive enough
      return error('Key not known. Please ask reset password.')
    if user_login is not None and register_user_login != user_login:
      # XXX: not descriptive enough
      return error("Bad login provided.")
    if DateTime() > expiration_date:
      # XXX: incorrect grammar
      return error("Date has expire.")
    del self._password_request_dict[password_key]
Vincent Pelletier's avatar
Vincent Pelletier committed
284 285 286 287 288 289 290 291 292
    portal = self.getPortalObject()
    user_dict, = portal.acl_users.searchUsers(
      login=register_user_login,
      exact_match=True,
    )
    login_dict, = user_dict['login_list']
    login = portal.unrestrictedTraverse(login_dict['path'])
    login._forceSetPassword(password)
    login.reindexObject()
293 294
    return redirect(REQUEST, site_url,
                    translateString("Password changed."))
295

296
InitializeClass(PasswordTool)