Implémentation complète (mais minimale) de la réinitialisation de mot de passe.
[Plinn.git] / RegistrationTool.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # © 2005-2013 Benoît PIN <pin@cri.ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Plinn registration tool: implements 3 modes to register members:
21 anonymous, manager, reviewed.
22
23
24
25 """
26
27 from Globals import InitializeClass, PersistentMapping
28 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
29 from Products.CMFDefault.RegistrationTool import RegistrationTool as BaseRegistrationTool
30 from AccessControl import ClassSecurityInfo, ModuleSecurityInfo
31 from AccessControl.Permission import Permission
32 from BTrees.OOBTree import OOBTree
33 from Products.CMFCore.permissions import ManagePortal, AddPortalMember
34 from Products.CMFCore.exceptions import AccessControl_Unauthorized
35 from Products.CMFCore.utils import getToolByName
36 from Products.CMFCore.utils import getUtilityByInterfaceName
37 from Products.GroupUserFolder.GroupsToolPermissions import ManageGroups
38 from Products.Plinn.utils import Message as _
39 from Products.Plinn.utils import encodeQuopriEmail
40 from DateTime import DateTime
41 from types import TupleType, ListType
42 from uuid import uuid4
43 from quopri import encodestring
44
45 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
46 MODE_ANONYMOUS = 'anonymous'
47 security.declarePublic('MODE_ANONYMOUS')
48
49 MODE_MANAGER = 'manager'
50 security.declarePublic('MODE_MANAGER')
51
52 MODE_REVIEWED = 'reviewed'
53 security.declarePublic('MODE_REVIEWED')
54
55 MODES = [MODE_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED]
56 security.declarePublic('MODES')
57
58 DEFAULT_MEMBER_GROUP = 'members'
59 security.declarePublic('DEFAULT_MEMBER_GROUP')
60
61
62
63 class RegistrationTool(BaseRegistrationTool) :
64
65 """ Create and modify users by making calls to portal_membership.
66 """
67
68 meta_type = "Plinn Registration Tool"
69
70 manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
71 BaseRegistrationTool.manage_options
72
73 security = ClassSecurityInfo()
74
75 security.declareProtected( ManagePortal, 'manage_regmode' )
76 manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(),
77 __name__='manage_regmode')
78
79 def __init__(self) :
80 self._mode = MODE_ANONYMOUS
81 self._chain = ''
82 self._passwordResetRequests = OOBTree()
83
84 security.declareProtected(ManagePortal, 'configureTool')
85 def configureTool(self, registration_mode, chain, REQUEST=None) :
86 """ """
87
88 if registration_mode not in MODES :
89 raise ValueError, "Unknown mode: " + registration_mode
90 else :
91 self._mode = registration_mode
92 self._updatePortalRoleMappingForMode(registration_mode)
93
94 wtool = getToolByName(self, 'portal_workflow')
95
96 if registration_mode == MODE_REVIEWED :
97 if not hasattr(wtool, '_chains_by_type') :
98 wtool._chains_by_type = PersistentMapping()
99 wfids = []
100 chain = chain.strip()
101
102 if chain == '(Default)' :
103 try : del wtool._chains_by_type['Member Data']
104 except KeyError : pass
105 self._chain = chain
106 else :
107 for wfid in chain.replace(',', ' ').split(' ') :
108 if wfid :
109 if not wtool.getWorkflowById(wfid) :
110 raise ValueError, '"%s" is not a workflow ID.' % wfid
111 wfids.append(wfid)
112
113 wtool._chains_by_type['Member Data'] = tuple(wfids)
114 self._chain = ', '.join(wfids)
115 else :
116 wtool._chains_by_type['Member Data'] = tuple()
117
118 if REQUEST :
119 REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
120
121 def _updatePortalRoleMappingForMode(self, mode) :
122
123 urlTool = getToolByName(self, 'portal_url')
124 portal = urlTool.getPortalObject()
125
126 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
127 portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1)
128 elif mode == MODE_MANAGER :
129 portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0)
130
131 security.declarePublic('getMode')
132 def getMode(self) :
133 # """ return current mode """
134 return self._mode[:]
135
136 security.declarePublic('getWfId')
137 def getWfChain(self) :
138 # """ return current workflow id """
139 return self._chain
140
141 security.declarePublic('roleMappingMismatch')
142 def roleMappingMismatch(self) :
143 # """ test if the role mapping is correct for the currrent mode """
144
145 mode = self._mode
146 urlTool = getToolByName(self, 'portal_url')
147 portal = urlTool.getPortalObject()
148
149 def rolesOfAddPortalMemberPerm() :
150 p=Permission(AddPortalMember, [], portal)
151 return p.getRoles()
152
153 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
154 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
155
156 elif mode == MODE_MANAGER :
157 roles = rolesOfAddPortalMemberPerm()
158 if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType :
159 return False
160
161 return True
162
163 security.declareProtected(AddPortalMember, 'addMember')
164 def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) :
165 """ Idem CMFCore but without default role """
166 BaseRegistrationTool.addMember(self, id, password, roles=roles,
167 domains=domains, properties=properties)
168
169 if self.getMode() in [MODE_ANONYMOUS, MODE_MANAGER] :
170 gtool = getToolByName(self, 'portal_groups')
171 mtool = getToolByName(self, 'portal_membership')
172 utool = getToolByName(self, 'portal_url')
173 portal = utool.getPortalObject()
174 isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
175 aclu = self.aq_inner.acl_users
176
177 for gid in groups:
178 g = gtool.getGroupById(gid)
179 if not isGrpManager :
180 if gid != DEFAULT_MEMBER_GROUP:
181 raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
182
183 if g is None :
184 gtool.addGroup(gid)
185 aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
186 g = gtool.getGroupById(gid)
187 g.addMember(id)
188
189
190 def afterAdd(self, member, id, password, properties):
191 """ notify member creation """
192 member.notifyWorkflowCreated()
193 member.indexObject()
194
195
196 security.declarePublic('requestPasswordReset')
197 def requestPasswordReset(self, userid):
198 """ add uuid / (userid, expiration) pair and return uuid """
199 self.clearExpiredPasswordResetRequests()
200 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
201 member = mtool.getMemberById(userid)
202 if member :
203 uuid = str(uuid4())
204 self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
205 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
206 ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
207 # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
208 # wrappé. Un « unrestrictedTraverse » ne marche pas.
209 # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
210 portal = utool.getPortalObject()
211 mailhost = portal.MailHost
212 sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
213 to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
214 subject = "=?utf-8?q?%s?=" % encodestring('Password reset')
215 lines = []
216 pr = lines.append
217 pr('%s/password_reset_form/%s' % (utool(), uuid))
218 body = '\n'.join(lines)
219 message = self.echange_mail_template(From=sender,
220 To=to,
221 Subject=subject,
222 ContentType = 'text/plain',
223 charset = 'UTF-8',
224 body=body)
225 mailhost.send(message)
226
227 security.declarePrivate('clearExpiredPasswordResetRequests')
228 def clearExpiredPasswordResetRequests(self):
229 now = DateTime()
230 for uuid, record in self._passwordResetRequests.items() :
231 userid, date = record
232 if date < now :
233 del self._passwordResetRequests[uuid]
234
235
236 security.declarePublic('resetPassword')
237 def resetPassword(self, uuid, password, confirm) :
238 record = self._passwordResetRequests.get(uuid)
239 if not record :
240 return _('Invalid reset password request.')
241
242 userid, expiration = record
243 now = DateTime()
244 if expiration < now :
245 self.clearExpiredPasswordResetRequests()
246 return _('Your reset password request has expired. You can ask a new one.')
247
248 msg = self.testPasswordValidity(password, confirm=confirm)
249 if not msg : # None if everything ok. Err message otherwise.
250 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
251 member = mtool.getMemberById(userid)
252 if member :
253 member.setSecurityProfile(password=password)
254 del self._passwordResetRequests[uuid]
255 return _('Password successfully resetted.')
256 else :
257 return _('"%s" username not found.') % userid
258
259
260 InitializeClass(RegistrationTool)