Mercurial > libervia-backend
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 |