document.erp5.Person.py 12.3 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
3 4 5
# Copyright (c) 2002-2005 Nexedi SARL and Contributors. All Rights Reserved.
#                         Jean-Paul Smets-Solanes <jp@nexedi.com>
#                         Kevin Deldycke <kevin_AT_nexedi_DOT_com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# 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 zope.interface
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31
from AccessControl import ClassSecurityInfo
32
from Products.ERP5Type import Permissions, PropertySheet, interfaces
33
from Products.ERP5.Document.Node import Node
34
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
35 36 37
from erp5.component.mixin.EncryptedPasswordMixin import EncryptedPasswordMixin
from erp5.component.mixin.LoginAccountProviderMixin import LoginAccountProviderMixin
from erp5.component.mixin.ERP5UserMixin import ERP5UserMixin
38
from Products.ERP5Type.Core.Workflow import ValidationFailed
39 40
from Products.CMFCore.utils import _checkPermission
from Products.CMFCore.exceptions import AccessControl_Unauthorized
Jean-Paul Smets's avatar
Jean-Paul Smets committed
41

42 43 44 45
try:
  from Products import PluggableAuthService
except ImportError:
  PluggableAuthService = None
46 47 48
else:
  from Products.ERP5Security.ERP5UserManager import ERP5UserManager
  from Products.ERP5Security.ERP5LoginUserManager import ERP5LoginUserManager
49

50 51 52 53 54 55 56 57 58

class UserExistsError(
    ValidationFailed,
    # to workaround pylint's false positive:
    #   Exception doesn't inherit from standard "Exception" class (nonstandard-exception)
    # because it cannot import ValidationFailed (which is set by a monkey patch), we also
    # inherit from Exception.
    Exception,
  ):
59 60 61
  def __init__(self, user_id):
    super(UserExistsError, self).__init__('user id %s already exists' % (user_id, ))

62

63
class Person(EncryptedPasswordMixin, Node, LoginAccountProviderMixin, ERP5UserMixin):
64
  """
Kevin Deldycke's avatar
Kevin Deldycke committed
65 66 67 68 69 70 71 72 73 74 75 76 77
      An Person object holds the information about
      an person (ex. you, me, someone in the company,
      someone outside of the company, a member of the portal,
      etc.).

      Person objects can contain Coordinate objects
      (ex. Telephone, Url) as well a documents of various types.

      Person objects can be synchronized accross multiple
      sites.

      Person objects inherit from the Node base class
      (one of the 5 base classes in the ERP5 universal business model)
78
  """
Kevin Deldycke's avatar
Kevin Deldycke committed
79

80 81 82
  meta_type = 'ERP5 Person'
  portal_type = 'Person'
  add_permission = Permissions.AddPortalContent
Kevin Deldycke's avatar
Kevin Deldycke committed
83

84
  zope.interface.implements(interfaces.INode)
85

86 87 88
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)
Kevin Deldycke's avatar
Kevin Deldycke committed
89

90 91 92 93 94 95 96 97 98 99 100
  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.CategoryCore
                    , PropertySheet.DublinCore
                    , PropertySheet.Reference
                    , PropertySheet.Person
                    , PropertySheet.Login
                    , PropertySheet.Mapping
                    , PropertySheet.Task
                    )
Kevin Deldycke's avatar
Kevin Deldycke committed
101

102 103
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getTitle')
104
  def getTitle(self, **kw):  # pylint: disable=super-on-old-class
105 106 107 108 109 110 111 112 113 114
    """
    Returns the title if it exists or a combination of
    first name, middle name and last name
    """
    title = ' '.join([x for x in (self.getFirstName(),
                                  self.getMiddleName(),
                                  self.getLastName()) if x])
    if title:
      return title
    return super(Person, self).getTitle(**kw)
115

116 117
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getTranslatedTitle')
118
  def getTranslatedTitle(self, **kw):  # pylint: disable=super-on-old-class
119 120 121 122 123 124 125 126 127 128
    """
    Returns the title if it exists or a combination of
    first name, middle name and last name
    """
    title = ' '.join([x for x in (self.getTranslatedFirstName(**kw),
                                  self.getTranslatedMiddleName(**kw),
                                  self.getTranslatedLastName(**kw)) if x])
    if title:
      return title
    return super(Person, self).getTranslatedTitle(**kw)
129

130 131 132 133
  security.declareProtected(Permissions.AccessContentsInformation,
                            'title_or_id')
  def title_or_id(self):
    return self.getTitleOrId()
Kevin Deldycke's avatar
Kevin Deldycke committed
134

135 136 137 138 139 140 141
  security.declareProtected(Permissions.AccessContentsInformation,
                            'hasTitle')
  def hasTitle(self):
    return self.hasFirstName() or \
        self.hasLastName() or \
        self.hasMiddleName() or \
        self._baseHasTitle()
142

143
  def __checkUserIdAvailability(self, pas_plugin_class, user_id=None, login=None, check_concurrent_execution=True):
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    # Encode reference to hex to prevent uppercase/lowercase conflict in
    # activity table (when calling countMessageWithTag)
    if user_id:
      tag = 'set_userid_' + user_id.encode('hex')
    else:
      tag = 'set_login_' + login.encode('hex')
    # Check that there no existing user
    acl_users = getattr(self, 'acl_users', None)
    if PluggableAuthService is not None and isinstance(acl_users,
          PluggableAuthService.PluggableAuthService.PluggableAuthService):
      plugin_name_set = {
        plugin_name for plugin_name, plugin_value in acl_users.plugins.listPlugins(
          PluggableAuthService.interfaces.plugins.IUserEnumerationPlugin,
        ) if isinstance(plugin_value, pas_plugin_class)
      }
      if plugin_name_set:
        if any(
          user['pluginid'] in plugin_name_set
          for user in acl_users.searchUsers(
            id=user_id,
            login=login,
            exact_match=True,
          )
        ):
168
          raise UserExistsError(user_id or login)
169
      else:
170 171 172 173
        # PAS is used, without expected enumeration plugin: property has no
        # effect on user enumeration, skip checks.
        # XXX: what if desired plugin becomes active later ?
        return
174 175 176

    if not check_concurrent_execution:
      return
177 178 179
    # Check that there is no reindexation related to reference indexation
    if self.getPortalObject().portal_activities.countMessageWithTag(tag):
      raise UserExistsError(user_id)
180

181 182 183 184 185 186 187 188 189 190 191 192 193 194
    # Prevent concurrent transaction to set the same reference on 2
    # different persons
    # XXX: person_module is rather large because of all permission
    # declarations, it would be better to find a smaller document to use
    # here.
    self.getParentValue().serialize()
    # Prevent to set the same reference on 2 different persons during the
    # same transaction
    transactional_variable = getTransactionalVariable()
    if tag in transactional_variable:
      raise UserExistsError(user_id)
    else:
      transactional_variable[tag] = None
    self.reindexObject(activate_kw={'tag': tag})
195

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
  def _setReference(self, value):
    """
    Set the user id. This method is defined explicitly, because
    we want to prevent duplicated user ids, but only when
    PAS _AND_ ERP5UserManager are used
    """
    if value != self.getReference():
      if value:
        self.__checkUserIdAvailability(
          pas_plugin_class=ERP5UserManager,
          login=value,
        )
      self._baseSetReference(value)
      # invalid the cache for ERP5Security
      self.getPortalObject().portal_caches.clearCache(cache_factory_list=('erp5_content_short', ))
211

212 213 214
  def _setUserId(self, value):
    """
    Set the user id. This method is defined explicitly, because:
215

216
      - we want to apply a different permission
217

218 219 220
      - we want to prevent duplicated user ids, but only when
        PAS _AND_ ERP5LoginUserManager are used
    """
221 222
    existing_user_id = self.getUserId()
    if value != existing_user_id:
223 224 225 226 227
      if value:
        self.__checkUserIdAvailability(
          pas_plugin_class=ERP5LoginUserManager,
          user_id=value,
        )
228 229
      if existing_user_id and not _checkPermission(Permissions.ManageUsers, self):
        raise AccessControl_Unauthorized('setUserId')
230
      self._baseSetUserId(value)
231

232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
  security.declareProtected(Permissions.ModifyPortalContent, 'initUserId')
  def initUserId(self):
    """Initialize user id.

    ERP5 guarantees unicity of user id when setUserId is called, but this
    comes at the expense of performance, because two transactions are not
    allowed to change any user id at a time.
    This implementation uses an id generator which already guarantees the
    unicity of generated values, so when using this method we can trust
    ourselves and don't need setUserId to check unicity of the user id
    we are generating with other concurrent generations.
    We ignore the risk that another concurrent transaction might be modifying
    a user with a conflicting user id (using another method than initUserId)
    and only check we are not generating an user id that would already be
    used before, in case some user ids were already set by other methods than
    initUserId - the most probable case being migration of persons created
    before introduction of user id and ERP5 Logins.

    If user id are really important in a project(which is very unlikely), this
    method can be customized in a type based method named Person_initUserId
    """
    method = self.getTypeBasedMethod('initUserId')
    if method is not None:
      return method()
    if not self.hasUserId():
      portal = self.getPortalObject()
      user_id = 'P%i' % portal.portal_ids.generateNewId(
          id_group='user_id',
          id_generator='non_continuous_integer_increasing',
      )
      self.__checkUserIdAvailability(
          pas_plugin_class=ERP5LoginUserManager,
          user_id=user_id,
          check_concurrent_execution=False
      )
      # until migration from ERP5UserManager -> ERP5UserManager is completed
      # we want to make sure we are not generating a user id that was used
      # as a reference of a not yet migrated person, otherwise we'll have a
      # duplicate when this person will be migrated.
      self.__checkUserIdAvailability(
          pas_plugin_class=ERP5UserManager,
          login=user_id,
          check_concurrent_execution=False
      )
      self._baseSetUserId(user_id)
      self.reindexObject()

279 280 281 282 283 284
  # Time management
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getAvailableTime')
  def getAvailableTime(self, *args, **kw):
    """
    Calculate available time for a person
285

286 287 288
    See SimulationTool.getAvailableTime
    """
    kw['node'] = [self.getUid()]
289

290 291
    portal_simulation = self.getPortalObject().portal_simulation
    return portal_simulation.getAvailableTime(*args, **kw)
292

293 294 295 296 297
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getAvailableTimeSequence')
  def getAvailableTimeSequence(self, *args, **kw):
    """
    Calculate available time for a person in a sequence
298

299 300 301
    See SimulationTool.getAvailableTimeSequence
    """
    kw['node'] = [self.getUid()]
302

303 304
    portal_simulation = self.getPortalObject().portal_simulation
    return portal_simulation.getAvailableTimeSequence(*args, **kw)
305

306 307 308 309 310 311
  # Notifiation API
  security.declareProtected(Permissions.AccessContentsInformation,
                            'notifyMessage')
  def notifyMessage(self, message):
    """
    This method can only be called with proxy roles.
312

313 314 315 316 317 318
    A per user preference allows for deciding how to be notified.
    - by email
    - by SMS (if meaningful)
    - daily
    - weekly
    - instantly
319

320 321
    notification is handled as an activity
    """