comparison src/plugins/plugin_misc_account.py @ 895:52ee240acc9c

plugin account: user can change his password or delete his XMPP account
author souliane <souliane@mailoo.org>
date Mon, 03 Mar 2014 10:00:15 +0100
parents e030460e065e
children 34dd9287dfe5
comparison
equal deleted inserted replaced
894:57c32d8ec847 895:52ee240acc9c
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _, D_
21 from logging import debug, info, warning, error 21 from logging import debug, info, warning, error
22 from sat.core import exceptions 22 from sat.core import exceptions
23 from twisted.internet import reactor, defer, protocol 23 from twisted.internet import reactor, defer, protocol
24 from os.path import join, dirname 24 from os.path import join, dirname
25 from twisted.python.procutils import which 25 from twisted.python.procutils import which
26 from twisted.python.failure import Failure 26 from twisted.python.failure import Failure
27 from email.mime.text import MIMEText 27 from email.mime.text import MIMEText
28 from twisted.mail.smtp import sendmail 28 from twisted.mail.smtp import sendmail
29 from sat.tools import xml_tools
29 30
30 PLUGIN_INFO = { 31 PLUGIN_INFO = {
31 "name": "Account Plugin", 32 "name": "Account Plugin",
32 "import_name": "MISC-ACCOUNT", 33 "import_name": "MISC-ACCOUNT",
33 "type": "MISC", 34 "type": "MISC",
54 "prosodyctl": "prosodyctl", 55 "prosodyctl": "prosodyctl",
55 "reserved_list": ['libervia'] # profiles which can't be used 56 "reserved_list": ['libervia'] # profiles which can't be used
56 } 57 }
57 58
58 59
60 class PasswordsMatchingError(Exception):
61 pass
62
63
59 class ProsodyRegisterProtocol(protocol.ProcessProtocol): 64 class ProsodyRegisterProtocol(protocol.ProcessProtocol):
60 """ Try to register an account with prosody """ 65 """ Try to register an account with prosody """
61 66
62 def __init__(self, password, deferred=None): 67 def __init__(self, password=None, deferred=None):
68 """
69 @param password: new user password
70 @param deferred
71 """
63 self.password = password 72 self.password = password
64 self.deferred = deferred 73 self.deferred = deferred
65 self.data = '' 74 self.data = ''
66 75
67 def connectionMade(self): 76 def connectionMade(self):
68 self.transport.write("%s\n%s" % ((self.password.encode('utf-8'), ) * 2)) 77 if self.password is None:
78 return
79 self.transport.write("%s\n%s" % ((self.password.encode('utf-8'),) * 2))
69 self.transport.closeStdin() 80 self.transport.closeStdin()
70 81
71 def outReceived(self, data): 82 def outReceived(self, data):
72 self.data += data 83 self.data += data
73 84
74 def errReceived(self, data): 85 def errReceived(self, data):
75 self.data += data 86 self.data += data
76 87
77 def processEnded(self, reason): 88 def processEnded(self, reason):
78 if (reason.value.exitCode == 0): 89 if (reason.value.exitCode == 0):
79 info(_('Prosody registration success')) 90 info(_('Prosody command succeed'))
80 self.deferred.callback(None) 91 self.deferred.callback(None)
81 else: 92 else:
82 error(_(u"Can't register Prosody account (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data}) 93 error(_(u"Can't complete Prosody command (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data})
83 self.deferred.errback(Failure(exceptions.InternalError)) 94 self.deferred.errback(Failure(exceptions.InternalError))
95
96 @classmethod
97 def prosodyctl(cls, plugin, command, password=None, profile=None):
98 """Create a new ProsodyRegisterProtocol and execute the given prosodyctl command.
99 @param plugin: instance of MiscAccount
100 @param command: the command to execute: "adduser", "passwd" or "deluser"
101 @param password: the user new password (leave to None for "deluser" command)
102 @param profile: the user profile
103 @return a Deferred instance
104 """
105 d = defer.Deferred()
106 prosody_reg = ProsodyRegisterProtocol(password, d)
107 prosody_exe = join(plugin._prosody_path, plugin.getConfig('prosodyctl'))
108 reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, command, "%s@%s" % (profile, plugin.getConfig('new_account_domain'))], path=plugin._prosody_path)
109 return d
84 110
85 111
86 class MiscAccount(object): 112 class MiscAccount(object):
87 """Account plugin: create a SàT + Prosody account, used by Libervia""" 113 """Account plugin: create a SàT + Prosody account, used by Libervia"""
88 #XXX: This plugin is a Q&D one used for the demo. Something more generic (and not 114 #XXX: This plugin is a Q&D one used for the demo. Something more generic (and not
91 def __init__(self, host): 117 def __init__(self, host):
92 info(_(u"Plugin Account initialization")) 118 info(_(u"Plugin Account initialization"))
93 self.host = host 119 self.host = host
94 host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True) 120 host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True)
95 host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self._getNewAccountDomain, async=False) 121 host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self._getNewAccountDomain, async=False)
122 host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False)
96 self._prosody_path = self.getConfig('prosody_path') 123 self._prosody_path = self.getConfig('prosody_path')
97 if self._prosody_path is None: 124 if self._prosody_path is None:
98 paths = which(self.getConfig('prosodyctl')) 125 paths = which(self.getConfig('prosodyctl'))
99 if not paths: 126 if not paths:
100 error(_("Can't find %s") % (self.getConfig('prosodyctl'), )) 127 error(_("Can't find %s") % (self.getConfig('prosodyctl'), ))
101 else: 128 else:
102 self._prosody_path = dirname(paths[0]) 129 self._prosody_path = dirname(paths[0])
103 info(_('Prosody path found: %s') % (self._prosody_path, )) 130 info(_('Prosody path found: %s') % (self._prosody_path, ))
104 131
132 self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True)
133 self.__delete_account_id = host.registerCallback(self.__deleteAccountCb, with_data=True)
134
105 def getConfig(self, name): 135 def getConfig(self, name):
106 return self.host.memory.getConfig(CONFIG_SECTION, name) or default_conf[name] 136 return self.host.memory.getConfig(CONFIG_SECTION, name) or default_conf[name]
107 137
108 def _registerAccount(self, email, password, profile): 138 def _registerAccount(self, email, password, profile):
109 139
136 "Connection", profile_key=profile) 166 "Connection", profile_key=profile)
137 #and the account 167 #and the account
138 168
139 #XXX: we use "prosodyctl adduser" because "register" doesn't check conflict 169 #XXX: we use "prosodyctl adduser" because "register" doesn't check conflict
140 # and just change the password if the account already exists 170 # and just change the password if the account already exists
141 d = defer.Deferred() 171 d = ProsodyRegisterProtocol.prosodyctl(self, 'adduser', password, profile)
142 prosody_reg = ProsodyRegisterProtocol(password, d)
143 prosody_exe = join(self._prosody_path, self.getConfig('prosodyctl'))
144 reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, 'adduser', "%s@%s" % (profile, self.getConfig('new_account_domain'))], path=self._prosody_path)
145
146 d.addCallback(self._sendEmails, profile, email, password) 172 d.addCallback(self._sendEmails, profile, email, password)
147 d.addCallback(lambda ignore: None) 173 d.addCallback(lambda ignore: None)
148 return d 174 return d
149 175
150 def _sendEmails(self, result, login, email, password): 176 def _sendEmails(self, result, login, email, password):
202 228
203 def _getNewAccountDomain(self): 229 def _getNewAccountDomain(self):
204 """@return: the domain that will be set to new account""" 230 """@return: the domain that will be set to new account"""
205 return self.getConfig('new_account_domain') 231 return self.getConfig('new_account_domain')
206 232
233 def _getAccountDialogUI(self, profile):
234 """Get the main dialog to manage your account
235 @param menu_data
236 @param profile: %(doc_profile)s
237 @return XML of the dialog
238 """
239 form_ui = xml_tools.XMLUI("form", "tabs", title=D_("Manage your XMPP account"), submit_id=self.__account_cb_id)
240 tab_container = form_ui.current_container
241 tab_container.addTab("update", D_("Change your password"), container=xml_tools.PairsContainer)
242 form_ui.addLabel(D_("Current password"))
243 form_ui.addPassword("current_passwd", value="")
244 form_ui.addLabel(D_("New password"))
245 form_ui.addPassword("new_passwd1", value="")
246 form_ui.addLabel(D_("New password (again)"))
247 form_ui.addPassword("new_passwd2", value="")
248 tab_container.addTab("delete", D_("Delete your account"), container=xml_tools.PairsContainer)
249 form_ui.addLabel(D_("Current password"))
250 form_ui.addPassword("delete_passwd", value="")
251 form_ui.addLabel(D_("Delete your account"))
252 form_ui.addBool("delete_checkbox", "false")
253 return form_ui.toXml()
254
255 def _accountDialogCb(self, data, profile):
256 """Called when the user submits the main account dialog
257 @param data
258 @param profile
259 """
260 password = self.host.memory.getParamA("Password", "Connection", profile_key=profile)
261
262 def error_ui():
263 error_ui = xml_tools.XMLUI("popup", title="Error")
264 error_ui.addText(D_("Passwords don't match!"))
265 return defer.succeed({'xmlui': error_ui.toXml()})
266
267 # check for account deletion
268 delete_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_passwd']
269 delete_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_checkbox']
270 if delete_checkbox == 'true':
271 if password == delete_passwd:
272 return self.__deleteAccount(profile)
273 return error_ui()
274
275 # check for password modification
276 current_passwd = data[xml_tools.SAT_FORM_PREFIX + 'current_passwd']
277 new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd1']
278 new_passwd2 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd2']
279 if new_passwd1 or new_passwd2:
280 if password == current_passwd and new_passwd1 == new_passwd2:
281 return self.__changePassword(new_passwd1, profile=profile)
282 return error_ui()
283
284 return defer.succeed({})
285
286 def __changePassword(self, password, profile):
287 """Actually change the user XMPP account and SàT profile password
288 @param password: new password
289 @profile
290 """
291 def passwordChanged(result):
292 self.host.memory.setParam("Password", password, "Connection", profile_key=profile)
293 confirm_ui = xml_tools.XMLUI("popup", title="Confirmation")
294 confirm_ui.addText(D_("Your password has been changed."))
295 return defer.succeed({'xmlui': confirm_ui.toXml()})
296
297 def errback(failure):
298 error_ui = xml_tools.XMLUI("popup", title="Error")
299 error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage())
300 return defer.succeed({'xmlui': error_ui.toXml()})
301
302 d = ProsodyRegisterProtocol.prosodyctl(self, 'passwd', password, profile=profile)
303 d.addCallbacks(passwordChanged, errback)
304 return d
305
306 def __deleteAccount(self, profile):
307 """Ask for a confirmation before deleting the XMPP account and SàT profile
308 @param profile
309 """
310 form_ui = xml_tools.XMLUI("form", title=D_("Delete your account ?"), submit_id=self.__delete_account_id)
311 form_ui.addText(D_("If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED."))
312 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
313 return {'xmlui': form_ui.toXml()}
314
315 def __deleteAccountCb(self, data, profile):
316 """Actually delete the XMPP account and SàT profile
317 @param data
318 @param profile
319 """
320 def userDeleted(result):
321 self.host.disconnect(profile)
322 self.host.memory.asyncDeleteProfile(profile, force=True)
323 return defer.succeed({})
324
325 def errback(failure):
326 error_ui = xml_tools.XMLUI("popup", title="Error")
327 error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage())
328 return defer.succeed({'xmlui': error_ui.toXml()})
329
330 d = ProsodyRegisterProtocol.prosodyctl(self, 'deluser', profile=profile)
331 d.addCallbacks(userDeleted, errback)
332 return d