comparison src/plugins/plugin_misc_account.py @ 2176:61128d260eef

plugin account: removed dependency to Prosody/prosodyctl and properly use in-band registration instead
author Goffi <goffi@goffi.org>
date Thu, 09 Mar 2017 00:06:13 +0100
parents 33c8c4973743
children 09cfec4d8d19
comparison
equal deleted inserted replaced
2175:75002ac33801 2176:61128d260eef
24 from sat.tools import xml_tools 24 from sat.tools import xml_tools
25 from sat.memory.memory import Sessions 25 from sat.memory.memory import Sessions
26 from sat.memory.crypto import PasswordHasher 26 from sat.memory.crypto import PasswordHasher
27 from sat.core.constants import Const as C 27 from sat.core.constants import Const as C
28 import ConfigParser 28 import ConfigParser
29 from twisted.internet import reactor, defer, protocol 29 from twisted.internet import defer
30 from twisted.python.procutils import which
31 from twisted.python.failure import Failure 30 from twisted.python.failure import Failure
31 from twisted.words.protocols.jabber import jid
32 from twisted.mail.smtp import sendmail 32 from twisted.mail.smtp import sendmail
33 from os.path import join, dirname
34 from email.mime.text import MIMEText 33 from email.mime.text import MIMEText
35 34
36 import random 35 import random
36
37 # FIXME: this plugin code is old and need a cleaning
38 # TODO: account deletion/password change need testing
37 39
38 40
39 PLUGIN_INFO = { 41 PLUGIN_INFO = {
40 C.PI_NAME: "Account Plugin", 42 C.PI_NAME: "Account Plugin",
41 C.PI_IMPORT_NAME: "MISC-ACCOUNT", 43 C.PI_IMPORT_NAME: "MISC-ACCOUNT",
42 C.PI_TYPE: "MISC", 44 C.PI_TYPE: "MISC",
43 C.PI_PROTOCOLS: [], 45 C.PI_PROTOCOLS: [],
44 C.PI_DEPENDENCIES: [], 46 C.PI_DEPENDENCIES: ["XEP-0077"],
45 C.PI_RECOMMENDATIONS: ['GROUPBLOG'], 47 C.PI_RECOMMENDATIONS: ['GROUPBLOG'],
46 C.PI_MAIN: "MiscAccount", 48 C.PI_MAIN: "MiscAccount",
47 C.PI_HANDLER: "no", 49 C.PI_HANDLER: "no",
48 C.PI_DESCRIPTION: _(u"""SàT account creation""") 50 C.PI_DESCRIPTION: _(u"""SàT account creation""")
49 } 51 }
64 "email_auth": "false", 66 "email_auth": "false",
65 "email_admins_list": [], 67 "email_admins_list": [],
66 "admin_email": "", 68 "admin_email": "",
67 "new_account_server": "localhost", 69 "new_account_server": "localhost",
68 "new_account_domain": "example.net", 70 "new_account_domain": "example.net",
69 "prosody_path": None, # prosody path (where prosodyctl will be executed from), or None to automaticaly find it
70 "prosodyctl": "prosodyctl",
71 "reserved_list": ['libervia'] # profiles which can't be used 71 "reserved_list": ['libervia'] # profiles which can't be used
72 } 72 }
73 73
74 74
75 class PasswordsMatchingError(Exception):
76 pass
77
78
79 class ProsodyRegisterProtocol(protocol.ProcessProtocol):
80 """ Try to register an account with prosody """
81
82 def __init__(self, password=None, deferred=None):
83 """
84 @param password: new user password
85 @param deferred
86 """
87 self.password = password
88 self.deferred = deferred
89 self.data = ''
90
91 def connectionMade(self):
92 if self.password is None:
93 return
94 self.transport.write("%s\n%s" % ((self.password.encode('utf-8'),) * 2))
95 self.transport.closeStdin()
96
97 def outReceived(self, data):
98 self.data += data
99
100 def errReceived(self, data):
101 self.data += data
102
103 def processEnded(self, reason):
104 if (reason.value.exitCode == 0):
105 log.info(_('Prosody command succeed'))
106 self.deferred.callback(None)
107 else:
108 log.error(_(u"Can't complete Prosody command (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data})
109 self.deferred.errback(Failure(exceptions.InternalError))
110
111 @classmethod
112 def prosodyctl(cls, plugin, command, password=None, profile=None):
113 """Create a new ProsodyRegisterProtocol and execute the given prosodyctl command.
114
115 @param plugin: instance of MiscAccount
116 @param command: the command to execute: "adduser", "passwd" or "deluser"
117 @param password: the user new password (leave to None for "deluser" command)
118 @param profile: the user profile
119 @return: a Deferred instance
120 """
121 d = defer.Deferred()
122 prosody_reg = ProsodyRegisterProtocol(password, d)
123 prosody_exe = join(plugin._prosody_path, plugin.getConfig('prosodyctl'))
124 # TODO delete account which are not on the same host
125 reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, command, "%s@%s" % (profile, plugin.getConfig('new_account_domain'))], path=plugin._prosody_path)
126 return d
127
128
129 class MiscAccount(object): 75 class MiscAccount(object):
130 """Account plugin: create a SàT + Prosody account, used by Libervia""" 76 """Account plugin: create a SàT + XMPP account, used by Libervia"""
131 #XXX: This plugin is a Q&D one used for the demo. Something more generic (and not 77 # XXX: This plugin was initialy a Q&D one used for the demo.
132 # only focused on Prosody) is planed 78 # TODO: cleaning, separate email handling, more configuration/tests, fixes
79
133 80
134 def __init__(self, host): 81 def __init__(self, host):
135 log.info(_(u"Plugin Account initialization")) 82 log.info(_(u"Plugin Account initialization"))
136 self.host = host 83 self.host = host
137 host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True) 84 host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True)
138 host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self.getNewAccountDomain, async=False) 85 host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self.getNewAccountDomain, async=False)
139 host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False) 86 host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False)
140 host.bridge.addMethod("asyncConnectWithXMPPCredentials", ".plugin", in_sign='ss', out_sign='b', method=self.asyncConnectWithXMPPCredentials, async=True) 87 host.bridge.addMethod("asyncConnectWithXMPPCredentials", ".plugin", in_sign='ss', out_sign='b', method=self.asyncConnectWithXMPPCredentials, async=True)
141 self._prosody_path = self.getConfig('prosody_path')
142 if self._prosody_path is None:
143 paths = which(self.getConfig('prosodyctl'))
144 if not paths:
145 log.error(_(u"Can't find %s") % (self.getConfig('prosodyctl'),))
146 else:
147 self._prosody_path = dirname(paths[0])
148 log.info(_(u'Prosody path found: %s') % (self._prosody_path,))
149 88
150 self.fixEmailAdmins() 89 self.fixEmailAdmins()
151 self._sessions = Sessions() 90 self._sessions = Sessions()
152 91
153 self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True) 92 self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True)
254 - non empty value: bind this JID to the new sat profile 193 - non empty value: bind this JID to the new sat profile
255 - None or empty: register a new JID on the local XMPP server 194 - None or empty: register a new JID on the local XMPP server
256 @param profile 195 @param profile
257 @return: Deferred 196 @return: Deferred
258 """ 197 """
259 # XXX: we use "prosodyctl adduser" because "register" doesn't check conflict and just change the password if the account already exists
260 if jid_s: 198 if jid_s:
261 d = defer.succeed(None) 199 d = defer.succeed(None)
200 jid_ = jid.JID(jid_s)
262 else: 201 else:
263 d = ProsodyRegisterProtocol.prosodyctl(self, 'adduser', password, profile) 202 jid_s = profile + u"@" + self.getConfig('new_account_domain')
264 jid_s = "%s@%s" % (profile, self.getConfig('new_account_domain')) 203 jid_ = jid.JID(jid_s)
204 d = self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
265 205
266 def setParams(dummy): 206 def setParams(dummy):
267 self.host.memory.setParam("JabberID", jid_s, "Connection", profile_key=profile) 207 self.host.memory.setParam("JabberID", jid_s, "Connection", profile_key=profile)
268 d = self.host.memory.setParam("Password", password, "Connection", profile_key=profile) 208 d = self.host.memory.setParam("Password", password, "Connection", profile_key=profile)
269 return d 209 return d
479 def __changePasswordCb(self, data, profile): 419 def __changePasswordCb(self, data, profile):
480 """Actually change the user XMPP account and SàT profile password 420 """Actually change the user XMPP account and SàT profile password
481 @param data (dict) 421 @param data (dict)
482 @profile (str): %(doc_profile)s 422 @profile (str): %(doc_profile)s
483 """ 423 """
424 client = self.host.getClient(profile)
484 password = self._sessions.profileGet(data['session_id'], profile)['new_password'] 425 password = self._sessions.profileGet(data['session_id'], profile)['new_password']
485 del self._sessions[data['session_id']] 426 del self._sessions[data['session_id']]
486 427
487 def passwordChanged(result): 428 def passwordChanged(dummy):
488 d = self.host.memory.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=profile) 429 d = self.host.memory.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=profile)
489 d.addCallback(lambda dummy: self.host.memory.setParam("Password", password, "Connection", profile_key=profile)) 430 d.addCallback(lambda dummy: self.host.memory.setParam("Password", password, "Connection", profile_key=profile))
490 confirm_ui = xml_tools.XMLUI("popup", title=D_("Confirmation")) 431 confirm_ui = xml_tools.XMLUI("popup", title=D_("Confirmation"))
491 confirm_ui.addText(D_("Your password has been changed.")) 432 confirm_ui.addText(D_("Your password has been changed."))
492 return defer.succeed({'xmlui': confirm_ui.toXml()}) 433 return defer.succeed({'xmlui': confirm_ui.toXml()})
494 def errback(failure): 435 def errback(failure):
495 error_ui = xml_tools.XMLUI("popup", title=D_("Error")) 436 error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
496 error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage()) 437 error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage())
497 return defer.succeed({'xmlui': error_ui.toXml()}) 438 return defer.succeed({'xmlui': error_ui.toXml()})
498 439
499 d = ProsodyRegisterProtocol.prosodyctl(self, 'passwd', password, profile=profile) 440 d = self.host.plugins['XEP-0077'].changePassword(client, password)
500 d.addCallbacks(passwordChanged, errback) 441 d.addCallbacks(passwordChanged, errback)
501 return d 442 return d
502 443
503 def __deleteAccount(self, profile): 444 def __deleteAccount(self, profile):
504 """Ask for a confirmation before deleting the XMPP account and SàT profile 445 """Ask for a confirmation before deleting the XMPP account and SàT profile
511 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?")) 452 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
512 return {'xmlui': form_ui.toXml()} 453 return {'xmlui': form_ui.toXml()}
513 454
514 def __deleteAccountCb(self, data, profile): 455 def __deleteAccountCb(self, data, profile):
515 """Actually delete the XMPP account and SàT profile 456 """Actually delete the XMPP account and SàT profile
457
516 @param data 458 @param data
517 @param profile 459 @param profile
518 """ 460 """
519 def userDeleted(result): 461 client = self.host.getClient(profile)
520 client = self.host.profiles[profile] 462 def userDeleted(dummy):
521 463
464 # FIXME: client should be disconnected at this point, so 2 next loop should be removed (to be confirmed)
522 for jid_ in client.roster._jids: # empty roster 465 for jid_ in client.roster._jids: # empty roster
523 client.presence.unsubscribe(jid_) 466 client.presence.unsubscribe(jid_)
524 467
525 for jid_ in self.host.memory.getWaitingSub(profile): # delete waiting subscriptions 468 for jid_ in self.host.memory.getWaitingSub(profile): # delete waiting subscriptions
526 self.host.memory.delWaitingSub(jid_) 469 self.host.memory.delWaitingSub(jid_)
537 def errback(failure): 480 def errback(failure):
538 error_ui = xml_tools.XMLUI("popup", title=D_("Error")) 481 error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
539 error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage()) 482 error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage())
540 return defer.succeed({'xmlui': error_ui.toXml()}) 483 return defer.succeed({'xmlui': error_ui.toXml()})
541 484
542 d = ProsodyRegisterProtocol.prosodyctl(self, 'deluser', profile=profile) 485 d = self.host.plugins['XEP-0077'].removeRegistration(client, jid.JID(client.jid.host))
543 d.addCallbacks(userDeleted, errback) 486 d.addCallbacks(userDeleted, errback)
544 return d 487 return d
545 488
546 def __deleteBlogPosts(self, posts, comments, profile): 489 def __deleteBlogPosts(self, posts, comments, profile):
547 """Ask for a confirmation before deleting the blog posts 490 """Ask for a confirmation before deleting the blog posts