PasswordTool.py 12.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
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)
Aurel's avatar
Aurel committed
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
      message_text_format = 'text/plain'
196
    else:
197
      message_text_format = notification_message.getContentType()
198
      subject = notification_message.getTitle()
199
      if message_text_format == "text/html":
200 201 202
        message = notification_message.asEntireHTML(substitution_method_parameter_dict=message_dict)
      else:
        message = notification_message.asText(substitution_method_parameter_dict=message_dict)
203 204 205 206
      event_keyword_argument_dict={
        'resource':notification_message.getSpecialise(),
        'language':notification_message.getLanguage(),
      }
207 208

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

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

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

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


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


Aurel's avatar
Aurel committed
274 275
  def changeUserPassword(self, password, password_key, password_confirm=None,
                         user_login=None, REQUEST=None, **kw):
276
    """
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
277
    Reset the password for a given login
278
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
279 280 281 282 283 284 285 286 287 288 289 290 291
    # 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)
      if REQUEST is None:
        return message
      return REQUEST.RESPONSE.redirect(
        site_url + '/login_form?' + urlencode({
          'portal_status_message': message,
        })
      )
292

293 294 295 296
    if REQUEST is None:
      REQUEST = get_request()
    if self.getWebSiteValue():
      site_url = self.getWebSiteValue().absolute_url()
Vincent Pelletier's avatar
Vincent Pelletier committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    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]
    except KeyError:
      # 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]
    persons = self.getPortalObject().acl_users.erp5_users.getUserByLogin(
      register_user_login)
316
    person = persons[0]
317
    person._forceSetPassword(password)
318 319
    person.reindexObject()
    if REQUEST is not None:
Vincent Pelletier's avatar
Vincent Pelletier committed
320 321 322 323 324
      return REQUEST.RESPONSE.redirect(
        site_url + '/login_form?' + urlencode({
          'portal_status_message': translateString("Password changed."),
        })
      )
325

326
InitializeClass(PasswordTool)