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