plugin account: user can change his password or delete his XMPP account

This commit is contained in:
souliane 2014-03-03 10:00:15 +01:00
commit 5b2e60e22e

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _
from sat.core.i18n import _, D_
from logging import debug, info, warning, error
from sat.core import exceptions
from twisted.internet import reactor, defer, protocol
@ -26,6 +26,7 @@ from twisted.python.procutils import which
from twisted.python.failure import Failure
from email.mime.text import MIMEText
from twisted.mail.smtp import sendmail
from sat.tools import xml_tools
PLUGIN_INFO = {
"name": "Account Plugin",
@ -56,16 +57,26 @@ default_conf = {"email_from": "NOREPLY@example.net",
}
class PasswordsMatchingError(Exception):
pass
class ProsodyRegisterProtocol(protocol.ProcessProtocol):
""" Try to register an account with prosody """
def __init__(self, password, deferred=None):
def __init__(self, password=None, deferred=None):
"""
@param password: new user password
@param deferred
"""
self.password = password
self.deferred = deferred
self.data = ''
def connectionMade(self):
self.transport.write("%s\n%s" % ((self.password.encode('utf-8'), ) * 2))
if self.password is None:
return
self.transport.write("%s\n%s" % ((self.password.encode('utf-8'),) * 2))
self.transport.closeStdin()
def outReceived(self, data):
@ -76,12 +87,27 @@ class ProsodyRegisterProtocol(protocol.ProcessProtocol):
def processEnded(self, reason):
if (reason.value.exitCode == 0):
info(_('Prosody registration success'))
info(_('Prosody command succeed'))
self.deferred.callback(None)
else:
error(_(u"Can't register Prosody account (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data})
error(_(u"Can't complete Prosody command (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data})
self.deferred.errback(Failure(exceptions.InternalError))
@classmethod
def prosodyctl(cls, plugin, command, password=None, profile=None):
"""Create a new ProsodyRegisterProtocol and execute the given prosodyctl command.
@param plugin: instance of MiscAccount
@param command: the command to execute: "adduser", "passwd" or "deluser"
@param password: the user new password (leave to None for "deluser" command)
@param profile: the user profile
@return a Deferred instance
"""
d = defer.Deferred()
prosody_reg = ProsodyRegisterProtocol(password, d)
prosody_exe = join(plugin._prosody_path, plugin.getConfig('prosodyctl'))
reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, command, "%s@%s" % (profile, plugin.getConfig('new_account_domain'))], path=plugin._prosody_path)
return d
class MiscAccount(object):
"""Account plugin: create a SàT + Prosody account, used by Libervia"""
@ -93,6 +119,7 @@ class MiscAccount(object):
self.host = host
host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True)
host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self._getNewAccountDomain, async=False)
host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False)
self._prosody_path = self.getConfig('prosody_path')
if self._prosody_path is None:
paths = which(self.getConfig('prosodyctl'))
@ -102,6 +129,9 @@ class MiscAccount(object):
self._prosody_path = dirname(paths[0])
info(_('Prosody path found: %s') % (self._prosody_path, ))
self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True)
self.__delete_account_id = host.registerCallback(self.__deleteAccountCb, with_data=True)
def getConfig(self, name):
return self.host.memory.getConfig(CONFIG_SECTION, name) or default_conf[name]
@ -138,11 +168,7 @@ class MiscAccount(object):
#XXX: we use "prosodyctl adduser" because "register" doesn't check conflict
# and just change the password if the account already exists
d = defer.Deferred()
prosody_reg = ProsodyRegisterProtocol(password, d)
prosody_exe = join(self._prosody_path, self.getConfig('prosodyctl'))
reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, 'adduser', "%s@%s" % (profile, self.getConfig('new_account_domain'))], path=self._prosody_path)
d = ProsodyRegisterProtocol.prosodyctl(self, 'adduser', password, profile)
d.addCallback(self._sendEmails, profile, email, password)
d.addCallback(lambda ignore: None)
return d
@ -204,3 +230,103 @@ SàT team""" % {'login': login, 'password': password, 'jid': "%s@%s" % (login, s
"""@return: the domain that will be set to new account"""
return self.getConfig('new_account_domain')
def _getAccountDialogUI(self, profile):
"""Get the main dialog to manage your account
@param menu_data
@param profile: %(doc_profile)s
@return XML of the dialog
"""
form_ui = xml_tools.XMLUI("form", "tabs", title=D_("Manage your XMPP account"), submit_id=self.__account_cb_id)
tab_container = form_ui.current_container
tab_container.addTab("update", D_("Change your password"), container=xml_tools.PairsContainer)
form_ui.addLabel(D_("Current password"))
form_ui.addPassword("current_passwd", value="")
form_ui.addLabel(D_("New password"))
form_ui.addPassword("new_passwd1", value="")
form_ui.addLabel(D_("New password (again)"))
form_ui.addPassword("new_passwd2", value="")
tab_container.addTab("delete", D_("Delete your account"), container=xml_tools.PairsContainer)
form_ui.addLabel(D_("Current password"))
form_ui.addPassword("delete_passwd", value="")
form_ui.addLabel(D_("Delete your account"))
form_ui.addBool("delete_checkbox", "false")
return form_ui.toXml()
def _accountDialogCb(self, data, profile):
"""Called when the user submits the main account dialog
@param data
@param profile
"""
password = self.host.memory.getParamA("Password", "Connection", profile_key=profile)
def error_ui():
error_ui = xml_tools.XMLUI("popup", title="Error")
error_ui.addText(D_("Passwords don't match!"))
return defer.succeed({'xmlui': error_ui.toXml()})
# check for account deletion
delete_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_passwd']
delete_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_checkbox']
if delete_checkbox == 'true':
if password == delete_passwd:
return self.__deleteAccount(profile)
return error_ui()
# check for password modification
current_passwd = data[xml_tools.SAT_FORM_PREFIX + 'current_passwd']
new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd1']
new_passwd2 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd2']
if new_passwd1 or new_passwd2:
if password == current_passwd and new_passwd1 == new_passwd2:
return self.__changePassword(new_passwd1, profile=profile)
return error_ui()
return defer.succeed({})
def __changePassword(self, password, profile):
"""Actually change the user XMPP account and SàT profile password
@param password: new password
@profile
"""
def passwordChanged(result):
self.host.memory.setParam("Password", password, "Connection", profile_key=profile)
confirm_ui = xml_tools.XMLUI("popup", title="Confirmation")
confirm_ui.addText(D_("Your password has been changed."))
return defer.succeed({'xmlui': confirm_ui.toXml()})
def errback(failure):
error_ui = xml_tools.XMLUI("popup", title="Error")
error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage())
return defer.succeed({'xmlui': error_ui.toXml()})
d = ProsodyRegisterProtocol.prosodyctl(self, 'passwd', password, profile=profile)
d.addCallbacks(passwordChanged, errback)
return d
def __deleteAccount(self, profile):
"""Ask for a confirmation before deleting the XMPP account and SàT profile
@param profile
"""
form_ui = xml_tools.XMLUI("form", title=D_("Delete your account ?"), submit_id=self.__delete_account_id)
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."))
form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
return {'xmlui': form_ui.toXml()}
def __deleteAccountCb(self, data, profile):
"""Actually delete the XMPP account and SàT profile
@param data
@param profile
"""
def userDeleted(result):
self.host.disconnect(profile)
self.host.memory.asyncDeleteProfile(profile, force=True)
return defer.succeed({})
def errback(failure):
error_ui = xml_tools.XMLUI("popup", title="Error")
error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage())
return defer.succeed({'xmlui': error_ui.toXml()})
d = ProsodyRegisterProtocol.prosodyctl(self, 'deluser', profile=profile)
d.addCallbacks(userDeleted, errback)
return d