diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 4e4aec9f3c..ba5f1d5052 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1446,7 +1446,7 @@ tools: ### tools_adminpw() adminpw: - action_help: Change admin password + action_help: Change password of admin and root users api: PUT /adminpw configuration: authenticate: all diff --git a/locales/en.json b/locales/en.json index 8e1ee3331e..6ce22ca80b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -273,6 +273,7 @@ "migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0", "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", + "migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", @@ -289,6 +290,8 @@ "migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", + "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", + "migration_0006_done": "Your root password have been replaced by your admin password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", @@ -371,6 +374,7 @@ "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", + "root_password_desynchronized": "The admin password has been changed, but YunoHost was unable to propagate this on the root password !", "server_shutdown": "The server will shutdown", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_reboot": "The server will reboot", diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py new file mode 100644 index 0000000000..366363f223 --- /dev/null +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -0,0 +1,79 @@ +import spwd +import crypt +import random +import string +import subprocess + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from moulinette.utils.process import run_commands, check_output +from moulinette.utils.filesystem import append_to_file +from moulinette.authenticators.ldap import Authenticator +from yunohost.tools import Migration + +logger = getActionLogger('yunohost.migration') +SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] + +class MyMigration(Migration): + "Synchronize admin and root passwords" + + def migrate(self): + + new_hash = self._get_admin_hash() + self._replace_root_hash(new_hash) + + logger.info(m18n.n("migration_0006_done")) + + def backward(self): + pass + + @property + def mode(self): + + # If the root password is still a "default" value, + # then this is an emergency and migration shall + # be applied automatically + # + # Otherwise, as playing with root password is touchy, + # we set this as a manual migration. + return "auto" if self._is_root_pwd_listed(SMALL_PWD_LIST) else "manual" + + @property + def disclaimer(self): + if self._is_root_pwd_listed(SMALL_PWD_LIST): + return None + + return m18n.n("migration_0006_disclaimer") + + def _get_admin_hash(self): + """ + Fetch the admin hash from the LDAP db using slapcat + """ + admin_hash = check_output("slapcat \ + | grep 'dn: cn=admin,dc=yunohost,dc=org' -A20 \ + | grep userPassword -A2 \ + | tr -d '\n ' \ + | tr ':' ' ' \ + | awk '{print $2}' \ + | base64 -d \ + | sed 's/{CRYPT}//g'") + return admin_hash + + def _replace_root_hash(self, new_hash): + hash_root = spwd.getspnam("root").sp_pwd + + with open('/etc/shadow', 'r') as before_file: + before = before_file.read() + + with open('/etc/shadow', 'w') as after_file: + after_file.write(before.replace("root:" + hash_root, + "root:" + new_hash)) + + def _is_root_pwd_listed(self, pwd_list): + hash_root = spwd.getspnam("root").sp_pwd + + for password in pwd_list: + if hash_root == crypt.crypt(password, hash_root): + return True + return False diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 7ef029fd7b..a34b41545a 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -129,18 +129,32 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password from yunohost.utils.password import assert_password_is_strong_enough - + import spwd + assert_password_is_strong_enough("admin", new_password) + + new_hash = _hash_user_password(new_password) try: - auth.update("cn=admin", { - "userPassword": _hash_user_password(new_password), - }) + auth.update("cn=admin", { "userPassword": new_hash, }) except: logger.exception('unable to change admin password') raise MoulinetteError(errno.EPERM, m18n.n('admin_password_change_failed')) else: + # Write as root password + try: + hash_root = spwd.getspnam("root").sp_pwd + + with open('/etc/shadow', 'r') as before_file: + before = before_file.read() + + with open('/etc/shadow', 'w') as after_file: + after_file.write(before.replace("root:" + hash_root, + "root:" + new_hash.replace('{CRYPT}', ''))) + except IOError as e: + logger.warning(m18n.n('root_password_desynchronized')) + return logger.success(m18n.n('admin_password_changed'))