PasswordTool.py 11.9 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 39
import time, random
from hashlib import md5 as md5_new
40
from DateTime import DateTime
41
from Products.ERP5Type.Message import translateString
42
from Products.ERP5Type.Globals import PersistentMapping
43
from urllib import urlencode
44 45 46

class PasswordTool(BaseTool):
  """
Jérome Perrin's avatar
Jérome Perrin committed
47
    PasswordTool is used to allow a user to change its password
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  """
  title = 'Password Tool'
  id = 'portal_password'
  meta_type = 'ERP5 Password Tool'
  portal_type = 'Password Tool'
  allowed_types = ()

  # Declarative Security
  security = ClassSecurityInfo()

  security.declareProtected(Permissions.ManagePortal, 'manage_overview' )
  manage_overview = DTMLFile( 'explainPasswordTool', _dtmldir )


  _expiration_day = 1
63 64
  _password_request_dict = {}

65 66 67
  def __init__(self, id=None):
    if id is None:
      id = self.__class__.id
68
    self._password_request_dict = PersistentMapping()
69 70
    # XXX no call to BaseTool.__init__ ?
    # BaseTool.__init__(self, id)
71

Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
72 73
  security.declareProtected('Manage users', 'getResetPasswordKey')
  def getResetPasswordKey(self, user_login):
74 75 76 77
    # generate expiration date
    expiration_date = DateTime() + self._expiration_day

    # generate a random string
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
78
    key = self._generateUUID()
79 80 81 82 83 84 85 86 87
    # XXX before r26093, _password_request_dict was initialized by an OOBTree and
    # replaced by a dict on each request, so if it's data structure is not up
    # to date, we update it if needed
    if not isinstance(self._password_request_dict, PersistentMapping):
      LOG('ERP5.PasswordTool', INFO, 'Updating password_request_dict to'
                                     ' PersistentMapping')
      self._password_request_dict = PersistentMapping()

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

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

97 98 99 100 101 102 103 104
    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)
105 106
    return url

107 108 109 110 111
  security.declareProtected('Manage users', 'getResetPasswordUrl')
  def getExpirationDateForKey(self, key=None):
    return self._password_request_dict[key][1]


Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
112
  def mailPasswordResetRequest(self, user_login=None, REQUEST=None,
113 114
                              notification_message=None, sender=None,
                              store_as_event=False):
115
    """
Jérome Perrin's avatar
Jérome Perrin committed
116
    Create a random string and expiration date for request
117 118 119
    Parameters:
    user_login -- Reference of the user to send password reset link
    REQUEST -- Request object
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
120
    notification_message -- Notification Message Document used to build the email.
121 122
                            As default, a standart text will be used.
    sender -- Sender (Person or Organisation) of the email.
123 124 125 126
            As default, the default email address will be used
    store_as_event -- whenever CRM is available, store
                        notifications as events
    """
127 128 129
    if REQUEST is None:
      REQUEST = get_request()

130 131 132
    if user_login is None:
      user_login = REQUEST["user_login"]

133 134 135 136
    site_url = self.getPortalObject().absolute_url()
    if REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from

137 138
    msg = None
    # check user exists, and have an email
139 140
    user_list = self.getPortalObject().acl_users.\
                      erp5_users.getUserByLogin(user_login)
141
    if len(user_list) == 0:
Yusei Tahara's avatar
Yusei Tahara committed
142
      msg = translateString("User ${user} does not exist.",
143
                            mapping={'user':user_login})
144 145 146 147 148 149 150 151 152 153 154
    else:
      # We use checked_permission to prevent errors when trying to acquire
      # email from organisation
      user = user_list[0]
      email_value = user.getDefaultEmailValue(
              checked_permission='Access content information')
      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:
155
      if REQUEST is not None:
156 157
        parameter = urlencode(dict(portal_status_message=msg))
        ret_url = '%s/login_form?%s' % \
158
                  (site_url, parameter)
159
        return REQUEST.RESPONSE.redirect( ret_url )
160
      return msg
161

Aurel's avatar
Aurel committed
162 163
    key = self.getResetPasswordKey(user_login=user_login)
    url = self.getResetPasswordUrl(key=key, site_url=site_url)
164 165

    # send mail
166 167
    message_dict = {'instance_name':self.getPortalObject().getTitle(),
                    'reset_password_link':url,
168
                    'expiration_date':self.getExpirationDateForKey(key).strftime('%Y/%m/%d %H:%M')}
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192

    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()
    else:
      subject = notification_message.getTitle()
      if notification_message.getContentType() == "text/html":
        message = notification_message.asEntireHTML(substitution_method_parameter_dict=message_dict)
      else:
        message = notification_message.asText(substitution_method_parameter_dict=message_dict)

    self.getPortalObject().portal_notifications.sendMessage(sender=sender, recipient=[user,],
193 194
                                                            subject=subject, message=message,
                                                            store_as_event=store_as_event)
195
    if REQUEST is not None:
196
      msg = translateString("An email has been sent to you.")
197
      parameter = urlencode(dict(portal_status_message=msg))
198
      ret_url = '%s/login_form?%s' % (site_url, parameter)
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
      return REQUEST.RESPONSE.redirect( ret_url )

  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
216
    data = ' '.join((str(t), str(r), str(a), str(args)))
217
    data = md5_new(data).hexdigest()
218 219 220
    return data


221
  def resetPassword(self, reset_key=None, REQUEST=None):
222 223
    """
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
224
    # XXX-Aurel : is it used ?
225 226
    if REQUEST is None:
      REQUEST = get_request()
227
    user_login, expiration_date = self._password_request_dict.get(reset_key, (None, None))
228 229 230
    site_url = self.getPortalObject().absolute_url()
    if REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from
231
    if reset_key is None or user_login is None:
232
      ret_url = '%s/login_form' % site_url
233 234 235 236 237
      return REQUEST.RESPONSE.redirect( ret_url )

    # check date
    current_date = DateTime()
    if current_date > expiration_date:
238
      msg = translateString("Date has expire.")
239
      parameter = urlencode(dict(portal_status_message=msg))
240
      ret_url = '%s/login_form?%s' % (site_url, parameter)
241
      return REQUEST.RESPONSE.redirect( ret_url )
242

243
    # redirect to form as all is ok
244
    REQUEST.set("password_key", reset_key)
245
    return self.reset_password_form(REQUEST=REQUEST)
246 247 248 249 250 251 252


  def removeExpiredRequests(self, **kw):
    """
    Browse dict and remove expired request
    """
    current_date = DateTime()
253
    for key, (login, date) in self._password_request_dict.items():
254
      if date < current_date:
255 256 257 258 259
        self._password_request_dict.pop(key)


  def changeUserPassword(self, user_login, password, password_confirmation,
                         password_key, REQUEST=None):
260
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
261
    Reset the password for a given login
262 263
    """
    # check the key
264 265
    register_user_login, expiration_date = self._password_request_dict.get(
                                                    password_key, (None, None))
266 267 268

    current_date = DateTime()
    msg = None
269 270 271 272 273 274 275
    if REQUEST is None:
      REQUEST = get_request()
    site_url = self.getPortalObject().absolute_url()
    if REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from
    if self.getWebSiteValue():
      site_url = self.getWebSiteValue().absolute_url()
276
    if register_user_login is None:
277
      msg = "Key not known. Please ask reset password."
278
    elif register_user_login != user_login:
279
      msg = translateString("Bad login provided.")
280
    elif current_date > expiration_date:
281
      msg = translateString("Date has expire.")
282
    elif not password:
Jérome Perrin's avatar
Jérome Perrin committed
283
      msg = translateString("Password must be entered.")
284
    elif password != password_confirmation:
Yusei Tahara's avatar
Yusei Tahara committed
285
      msg = translateString("Passwords do not match.")
286 287
    if msg is not None:
      if REQUEST is not None:
288
        parameter = urlencode(dict(portal_status_message=msg))
289
        ret_url = '%s/login_form?%s' % (site_url, parameter)
290 291 292 293 294
        return REQUEST.RESPONSE.redirect( ret_url )
      else:
        return msg

    # all is OK, change password and remove it from request dict
295 296
    self._password_request_dict.pop(password_key)
    persons = self.getPortalObject().acl_users.erp5_users.getUserByLogin(user_login)
297
    person = persons[0]
298
    person._forceSetPassword(password)
299 300
    person.reindexObject()
    if REQUEST is not None:
301
      msg = translateString("Password changed.")
302
      parameter = urlencode(dict(portal_status_message=msg))
303
      ret_url = '%s/login_form?%s' % (site_url, parameter)
304
      return REQUEST.RESPONSE.redirect( ret_url )
305

306
InitializeClass(PasswordTool)