#!/usr/bin/python3
#
# Univention Samba Machine Password Rotation Script
#
# SPDX-FileCopyrightText: 2013-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only


import os
import struct
import subprocess
import sys
import time
import traceback

import tdb

from univention.config_registry import ConfigRegistry


def write_machine_secret_to_secrets_tdb(machine_password, windows_domain):
    machine_password_key = b'SECRETS/MACHINE_PASSWORD/%s' % windows_domain.encode('UTF-8')
    previous_machine_password_key = b'SECRETS/MACHINE_PASSWORD.PREV/%s' % windows_domain.encode('UTF-8')
    machine_last_change_key = b'SECRETS/MACHINE_LAST_CHANGE_TIME/%s' % windows_domain.encode('UTF-8')

    if os.path.exists('/var/lib/samba/private/secrets.tdb'):
        secrets_tdb_filename = '/var/lib/samba/private/secrets.tdb'
    else:
        secrets_tdb_filename = '/var/lib/samba/secrets.tdb'

    secrets_tdb = tdb.open(secrets_tdb_filename)
    secrets_tdb.transaction_start()
    try:
        previous_machine_password = secrets_tdb.get(machine_password_key)
        secrets_tdb.store(previous_machine_password_key, previous_machine_password)

        secrets_tdb.store(machine_password_key, b"%s\0" % (machine_password.encode('UTF-8'),))

        seconds_since_epoch = int(time.mktime(time.localtime()))
        seconds_since_epoch_uint32 = struct.pack("<L", seconds_since_epoch)
        assert len(seconds_since_epoch_uint32) == 4
        secrets_tdb.store(machine_last_change_key, seconds_since_epoch_uint32)

        secrets_tdb.transaction_commit()
    except BaseException:
        secrets_tdb.transaction_cancel()
        raise
    finally:
        secrets_tdb.close()


def run_postchange():
    windows_domain = ucr.get('windows/domain', '').upper()
    with open('/etc/machine.secret') as fd:
        machine_password = fd.read().strip()
    ldap_hostdn = ucr.get('ldap/hostdn')
    samba_user = ucr.get('samba/user')
    samba_role = ucr.get('samba/role')
    server_role = ucr.get('server/role')
    ldap_base = ucr.get('ldap/base')

    if not windows_domain:
        sys.stdout.write("ERROR: windows/domain is not set!\n")
        return 1

    # store machine.secret in secrets.tdb
    try:
        write_machine_secret_to_secrets_tdb(machine_password, windows_domain)
    except BaseException:
        sys.stdout.write(traceback.format_exc())
        sys.stdout.flush()
        return 1
    else:
        sys.stdout.write("machine password stored successfully in secrets.tdb\n")
        sys.stdout.flush()

    idmap_domains = ['*']
    samba_idmap_domains = ucr.get('samba/idmap/domains')
    if samba_idmap_domains:
        idmap_domains.extend(samba_idmap_domains)

    if (server_role == 'domaincontroller_slave') or (samba_role == 'memberserver'):  # this is the criterion used in 26univention-samba.inst
        if samba_user == ldap_hostdn:
            with open('/etc/machine.secret') as fd:
                samba_user_secret = fd.read().strip()

            # store new machine secret as ldap bind password for passdb LDAP
            process = subprocess.Popen(['/usr/bin/smbpasswd', '-w', samba_user_secret])
            process.wait()

            for idmap_domain in idmap_domains:
                # store secret for idmap domain
                sys.stdout.write("setting idmap secret for '%s' from /etc/machine.secret\n" % idmap_domain)
                sys.stdout.flush()
                process = subprocess.Popen(['net', 'idmap', 'set', 'secret', idmap_domain, machine_password])
                process.wait()
        else:
            # don't touch anything, just issue a warning
            sys.stdout.write("WARNING: samba/user is expected to be set to the ldap/hostdn on UCS Replica Directory Nodes and UCS Managed Nodes.\n")
            sys.stdout.write("WARNING: samba/user is '%s' instead, skipping update of idmap secrets.\n" % samba_user)
    else:
        # don't touch anything, just check that things are sane:
        default_samba_user = "cn=admin,%s" % ldap_base
        if samba_user != default_samba_user:
            special_dc_roles = {'domaincontroller_master': 'UCS Primary Directory Node', 'domaincontroller_backup': 'UCS Backup Directory Node'}
            if server_role in special_dc_roles:
                sys.stdout.write("WARNING: samba/user is expected to be set to the %s on a %s.\n" % (default_samba_user, special_dc_roles[server_role]))
                sys.stdout.write("WARNING: samba/user is '%s' instead. Anyway, this is just a warning.\n" % samba_user)
            else:
                sys.stdout.write("WARNING: unexpected server role %s.\n" % server_role)

    for service in ('samba', 'winbind'):
        # restart services
        initscript = '/etc/init.d/%s' % service
        if os.path.isfile(initscript) and os.access(initscript, os.X_OK):
            if service == 'winbind':
                time.sleep(3)
            process = subprocess.Popen([initscript, 'restart'])
            process.wait()

    return 0


def run_prechange():
    windows_domain = ucr.get('windows/domain', '').upper()

    if not windows_domain:
        sys.stdout.write("ERROR: windows/domain is not set!\n")
        return 1

    return 0


if __name__ == '__main__':
    ucr = ConfigRegistry()
    ucr.load()

    if len(sys.argv) != 2:
        print("%s [prechange|nochange|postchange]" % sys.argv[0])
    else:
        if sys.argv[1] == "postchange":
            rc = run_postchange()
            sys.exit(rc)
        elif sys.argv[1] == "prechange":
            rc = run_prechange()
            sys.exit(rc)
        else:
            sys.exit(0)
