Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize root and admin password #527

Merged
merged 13 commits into from Nov 4, 2018
Merged
2 changes: 1 addition & 1 deletion data/actionsmap/yunohost.yml
Expand Up @@ -1438,7 +1438,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
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Expand Up @@ -364,6 +364,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": "Password of the user admin has been changed, but Root password has not been synchronized with your new admin 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",
Expand Down
132 changes: 132 additions & 0 deletions src/yunohost/data_migrations/0006_migrate_pwd.py
@@ -0,0 +1,132 @@
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
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", "olinux"]

class MyMigration(Migration):
"Migrate password"

def migrate(self):

if self._is_root_pwd_listed(SMALL_PWD_LIST):
new_hash = self._get_admin_hash()
self._replace_root_hash(new_hash)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rethinking about this, I think we need to clarify the purpose of this migration ? (Or maybe I'm kinda lost about it)

It seems like this PR does two different things :

  • check if the root password is "bad", and if so, resync it with the admin password
  • change tools_adminpw such that it also changes the root password each time the admin password is modified...

But for existing instance on which the root password ain't "bad", after this migration, we still end up with the root password and admin password being desynchronized. Shouldn't we sync the root and admin password in any case so that we end up with consistent setups in all cases ?


def backward(self):

pass

def _get_admin_hash(self):
"""
Ask for admin hash the ldap db
Note: to do that like we don't know the admin password we add a second
password
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing this and : is this method really needed to get the admin hash ?

I just realized that, as root, you can run slapcat from which you may extract the relevant info. Here's a quick and dirty command to do that (we might want to have a more robust way to do that though) :

slapcat | grep "dn: cn=admin,dc=yunohost,dc=org" -A20 | grep userPassword -A2 | tr -d '\n ' | tr ':' ' ' | awk '{print $2}' | base64 -d

"""
logger.debug('Generate a random temporary password')
tmp_password = ''.join(random.choice(string.ascii_letters +
string.digits) for i in range(12))

# Generate a random temporary password (won't be valid after this
# script ends !) and hash it
logger.debug('Hash temporary password')
tmp_hash = subprocess.check_output(["slappasswd", "-h", "{SSHA}","-s",
tmp_password])

try:
logger.debug('Stop slapd and backup its conf')
run_commands([
# Stop slapd service...
'systemctl stop slapd',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that one looks kinda touchy 😛 I wonder if that's really a good idea (though there are not so many alternatives). I'm guessing that the fact that we're logged as admin (e.g. from the webadmin) is cached somewhere (unscd?) so that should be okay ...

But we should reaaaally make sure that slapd gets back on at some point.... c.f. the "slapd start" at the end of the finally block... I wonder what happens if one of the previous command fails for some reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_commands raise a CalledProcessError at the first command that fail. We can disable this behavious by passing a specific callback that don't raise error.


# Backup slapd.conf (to be restored at the end of script)
'cp /etc/ldap/slapd.conf /root/slapd.conf.bkp'
])

logger.debug('Add password to the conf')
# Append lines to slapd.conf to manually define root password hash
append_to_file("/etc/ldap/slapd.conf", 'rootdn "cn=admin,dc=yunohost,dc=org"')
append_to_file("/etc/ldap/slapd.conf", "\n")
append_to_file("/etc/ldap/slapd.conf", 'rootpw ' + tmp_hash)

logger.debug('Start slapd with new password')
run_commands([
# Test conf (might not be entirely necessary though :P)
'slaptest -Q -u -f /etc/ldap/slapd.conf',

# Regenerate slapd.d directory
'rm -Rf /etc/ldap/slapd.d',
'mkdir /etc/ldap/slapd.d',
'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1',

# Set permissions to slapd.d
'chown -R openldap:openldap /etc/ldap/slapd.d/',

# Restore slapd.conf
'mv /root/slapd.conf.bkp /etc/ldap/slapd.conf',

# Restart slapd service
'service slapd start'
])

logger.debug('Authenticate on ldap')
auth = Authenticator('default', 'ldap://localhost:389',
'dc=yunohost,dc=org', 'cn=admin')
auth.authenticate( tmp_password)
logger.debug('Ask for the admin hash')
admin_hash = auth.search('cn=admin,dc=yunohost,dc=org', 'cn=admin',
['userPassword'])[0]['userPassword'][0]
admin_hash = admin_hash.replace('{CRYPT}', '')
finally:
logger.debug('Remove tmp_password from ldap db')
# Remove tmp_password from ldap db
run_commands([

# Stop slapd service
'service slapd stop || true',

'if [ -f /root/slapd.conf.bkp ]; then mv /root/slapd.conf.bkp /etc/ldap/slapd.conf; fi',

# Regenerate slapd.d directory
'rm -Rf /etc/ldap/slapd.d',
'mkdir /etc/ldap/slapd.d',
'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1',

# Set permissions to slapd.d
'chown -R openldap:openldap /etc/ldap/slapd.d/',

# Restart slapd service
'service slapd start'
])
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
19 changes: 16 additions & 3 deletions src/yunohost/tools.py
Expand Up @@ -127,15 +127,28 @@ def tools_adminpw(auth, new_password):

"""
from yunohost.user import _hash_user_password
import spwd
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))
except IOError as e:
logger.warning(m18n.n('root_password_desynchronized'))
return
logger.success(m18n.n('admin_password_changed'))


Expand Down