PasswordTool.py 12.6 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 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
  security.declareProtected('Manage users', 'getResetPasswordKey')
73 74 75 76
  def getResetPasswordKey(self, user_login, expiration_date=None):
    if expiration_date is None:
      # generate expiration date
      expiration_date = DateTime() + self._expiration_day
77 78

    # generate a random string
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
79
    key = self._generateUUID()
80 81 82 83 84 85 86 87 88
    # 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
89 90 91 92
    self._password_request_dict[key] = (user_login, expiration_date)
    return key

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

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

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


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

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

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

143 144
    msg = None
    # check user exists, and have an email
145 146
    user_list = self.getPortalObject().acl_users.\
                      erp5_users.getUserByLogin(user_login)
147
    if len(user_list) == 0:
Yusei Tahara's avatar
Yusei Tahara committed
148
      msg = translateString("User ${user} does not exist.",
149
                            mapping={'user':user_login})
150 151 152 153 154 155 156 157 158 159 160
    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:
161
      if REQUEST is not None:
162 163
        parameter = urlencode(dict(portal_status_message=msg))
        ret_url = '%s/login_form?%s' % \
164
                  (site_url, parameter)
165
        return REQUEST.RESPONSE.redirect( ret_url )
166
      return msg
167

168 169
    key = self.getResetPasswordKey(user_login=user_login,
                                   expiration_date=expiration_date)
170
    url = self.getResetPasswordUrl(key=key, site_url=site_url)
171 172

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

    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()
194
      event_keyword_argument_dict={}
195 196 197 198 199 200
    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)
201 202 203 204
      event_keyword_argument_dict={
        'resource':notification_message.getSpecialise(),
        'language':notification_message.getLanguage(),
      }
205 206

    self.getPortalObject().portal_notifications.sendMessage(sender=sender, recipient=[user,],
207
                                                            subject=subject, message=message,
208 209
                                                            store_as_event=store_as_event,
                                                            event_keyword_argument_dict=event_keyword_argument_dict)
210
    if REQUEST is not None:
211
      msg = translateString("An email has been sent to you.")
212
      parameter = urlencode(dict(portal_status_message=msg))
213
      ret_url = '%s/login_form?%s' % (site_url, parameter)
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
      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
231
    data = ' '.join((str(t), str(r), str(a), str(args)))
232
    return md5(data).hexdigest()
233

234
  def resetPassword(self, reset_key=None, REQUEST=None):
235 236
    """
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
237
    # XXX-Aurel : is it used ?
238 239
    if REQUEST is None:
      REQUEST = get_request()
240
    user_login, expiration_date = self._password_request_dict.get(reset_key, (None, None))
241 242 243
    site_url = self.getPortalObject().absolute_url()
    if REQUEST and 'came_from' in REQUEST:
      site_url = REQUEST.came_from
244
    if reset_key is None or user_login is None:
245
      ret_url = '%s/login_form' % site_url
246 247 248 249 250
      return REQUEST.RESPONSE.redirect( ret_url )

    # check date
    current_date = DateTime()
    if current_date > expiration_date:
251
      msg = translateString("Date has expire.")
252
      parameter = urlencode(dict(portal_status_message=msg))
253
      ret_url = '%s/login_form?%s' % (site_url, parameter)
254
      return REQUEST.RESPONSE.redirect( ret_url )
255

256
    # redirect to form as all is ok
257
    REQUEST.set("password_key", reset_key)
258
    return self.reset_password_form(REQUEST=REQUEST)
259 260 261 262 263 264 265


  def removeExpiredRequests(self, **kw):
    """
    Browse dict and remove expired request
    """
    current_date = DateTime()
266
    for key, (login, date) in self._password_request_dict.items():
267
      if date < current_date:
268 269 270
        self._password_request_dict.pop(key)


271 272
  def changeUserPassword(self, password, password_key, password_confirm=None,
                         user_login=None, REQUEST=None, **kw):
273
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
274
    Reset the password for a given login
275 276
    """
    # check the key
277 278
    register_user_login, expiration_date = self._password_request_dict.get(
                                                    password_key, (None, None))
279 280 281

    current_date = DateTime()
    msg = None
282 283 284 285 286 287 288
    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()
289
    if register_user_login is None:
290
      msg = "Key not known. Please ask reset password."
291
    elif user_login is not None and register_user_login != user_login:
292
      msg = translateString("Bad login provided.")
293
    elif current_date > expiration_date:
294
      msg = translateString("Date has expire.")
295 296
    if msg is not None:
      if REQUEST is not None:
297
        parameter = urlencode(dict(portal_status_message=msg))
298
        ret_url = '%s/login_form?%s' % (site_url, parameter)
299 300 301 302 303
        return REQUEST.RESPONSE.redirect( ret_url )
      else:
        return msg

    # all is OK, change password and remove it from request dict
304
    self._password_request_dict.pop(password_key)
305
    persons = self.getPortalObject().acl_users.erp5_users.getUserByLogin(register_user_login)
306
    person = persons[0]
307
    person._forceSetPassword(password)
308 309
    person.reindexObject()
    if REQUEST is not None:
310
      msg = translateString("Password changed.")
311
      parameter = urlencode(dict(portal_status_message=msg))
312
      ret_url = '%s/login_form?%s' % (site_url, parameter)
313
      return REQUEST.RESPONSE.redirect( ret_url )
314

315
InitializeClass(PasswordTool)