364 lines
		
	
	
		
			No EOL
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			364 lines
		
	
	
		
			No EOL
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import os, re, sys, configparser, subprocess, shutil
 | 
						|
from pathlib import Path
 | 
						|
from linuxmusterLinuxclient7 import logging, constants, hooks, shares, config, user, templates, realm, fileHelper, printers, computer
 | 
						|
 | 
						|
def setup(domain=None, user=None):
 | 
						|
    """
 | 
						|
    Sets up the client to be able to act in a linuxmuster environment
 | 
						|
 | 
						|
    :param domain: The domain to join, defaults to the first discovered domain
 | 
						|
    :type domain: str, optional
 | 
						|
    :param user: The admin user for the join, defaults to global-admin
 | 
						|
    :type user: str, optional
 | 
						|
    :return: True on success, False otherwise
 | 
						|
    :rtype: bool
 | 
						|
    """
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 setup ####')
 | 
						|
 | 
						|
    if not realm.clearUserCache():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _cleanOldDomainJoins():
 | 
						|
        return False
 | 
						|
 | 
						|
    rc, domain = _findDomain(domain)
 | 
						|
    if not rc:
 | 
						|
        return False
 | 
						|
 | 
						|
    if user == None:
 | 
						|
        user = constants.defaultDomainAdminUser
 | 
						|
 | 
						|
    if not _prepareNetworkConfiguration(domain):
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _deleteObsoleteFiles():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not templates.applyAll():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _preparePam():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _prepareServices():
 | 
						|
        return False
 | 
						|
 | 
						|
    # Actually join domain!
 | 
						|
    print()
 | 
						|
    logging.info(f"#### Joining domain {domain} ####")
 | 
						|
 | 
						|
    if not realm.join(domain, user):
 | 
						|
        return False
 | 
						|
 | 
						|
    # copy server ca certificate in place
 | 
						|
    # This will also make sure that the domain join actually worked;
 | 
						|
    # mounting the sysvol will fail otherwise.
 | 
						|
    if not _installCaCertificate(domain, user):
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _adjustSssdConfiguration(domain):
 | 
						|
        return False
 | 
						|
 | 
						|
    # run a final test
 | 
						|
    if not realm.verifyDomainJoin():
 | 
						|
        return False
 | 
						|
 | 
						|
    print("\n\n")
 | 
						|
 | 
						|
    logging.info(f"#### SUCCESSFULLY joined domain {domain} ####")
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def status():
 | 
						|
    """
 | 
						|
    Checks the status of the client
 | 
						|
 | 
						|
    :return: True on success, False otherwise
 | 
						|
    :rtype: bool
 | 
						|
    """
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 status ####')
 | 
						|
 | 
						|
    if not isSetup():
 | 
						|
        logging.info("Not setup!")
 | 
						|
        return False
 | 
						|
 | 
						|
    logging.info("Linuxmuster-linuxclient7 is setup!")
 | 
						|
    logging.info("Testing if domain is joined...")
 | 
						|
 | 
						|
    logging.info("Checking joined domains")
 | 
						|
    rc, joinedDomains = realm.getJoinedDomains()
 | 
						|
    if not rc:
 | 
						|
        return False
 | 
						|
 | 
						|
    print()
 | 
						|
    logging.info("Joined domains:")
 | 
						|
    for joinedDomain in joinedDomains:
 | 
						|
        logging.info(f"* {joinedDomain}")
 | 
						|
    print()
 | 
						|
 | 
						|
    if len(joinedDomains) > 0 and not realm.verifyDomainJoin():
 | 
						|
        print()
 | 
						|
        # Give a little explination to our users :)
 | 
						|
        print("\n===============================================================================================")
 | 
						|
        print("This Computer is joined to a domain, but it was not possible to authenticate")
 | 
						|
        print("to the domain controller. There is an error with your domain join! The login WILL NOT WORK!")
 | 
						|
        print("Please try to re-join the domain using 'linuxmuster-linuxclient7 setup' and create a new image.")
 | 
						|
        print("===============================================================================================\n")
 | 
						|
        return False
 | 
						|
    elif len(joinedDomains) <= 0:
 | 
						|
        print()
 | 
						|
        logging.info('#### This client is not joined to any domain. ####')
 | 
						|
        print("#### To join a domain, run \"linuxmuster-linuxclient7 setup\" ####")
 | 
						|
 | 
						|
    print()
 | 
						|
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 is fully setup and working! ####')
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def upgrade():
 | 
						|
    """
 | 
						|
    Performs an upgrade of the linuxmuster-linuxclient7. This is executed after the package is updated.
 | 
						|
 | 
						|
    :return: True on success, False otherwise
 | 
						|
    :rtype: bool
 | 
						|
    """
 | 
						|
    if not isSetup():
 | 
						|
        logging.info("linuxmuster-linuxclient7 does not seem to be setup -> no upgrade is needed")
 | 
						|
        return True
 | 
						|
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 upgrade ####')
 | 
						|
    if not config.upgrade():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _deleteObsoleteFiles():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not templates.applyAll():
 | 
						|
        return False
 | 
						|
 | 
						|
    if not _prepareServices():
 | 
						|
        return False
 | 
						|
 | 
						|
    rc, joinedDomains = realm.getJoinedDomains()
 | 
						|
    if not rc:
 | 
						|
        return False
 | 
						|
    
 | 
						|
    for domain in joinedDomains:
 | 
						|
        _adjustSssdConfiguration(domain)
 | 
						|
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 upgrade SUCCESSFULL ####')
 | 
						|
    return True
 | 
						|
 | 
						|
def clean():
 | 
						|
    """Removes all sensitive files like keys and leaves all domain joins.
 | 
						|
    """
 | 
						|
    logging.info("#### linuxmuster-linuxclient7 clean ####")
 | 
						|
 | 
						|
    realm.clearUserCache()
 | 
						|
    _cleanOldDomainJoins()
 | 
						|
 | 
						|
    # clean /etc/pam.d/common-session
 | 
						|
    logging.info("Cleaning /etc/pam.d/common-session to prevent logon brick")
 | 
						|
    fileHelper.removeLinesInFileContainingString("/etc/pam.d/common-session", ["pam_mkhomedir.so", "pam_exec.so", "pam_mount.so", "linuxmuster.net", "linuxmuster-linuxclient7", "linuxmuster-client-adsso"])
 | 
						|
 | 
						|
    logging.info('#### linuxmuster-linuxclient7 clean SUCCESSFULL ####')
 | 
						|
 | 
						|
def isSetup():
 | 
						|
    """
 | 
						|
    Checks if the client is setup.
 | 
						|
 | 
						|
    :return: True if setup, False otherwise
 | 
						|
    :rtype: bool
 | 
						|
    """
 | 
						|
    return os.path.isfile(constants.networkConfigFilePath) 
 | 
						|
 | 
						|
# --------------------
 | 
						|
# - Helper functions -
 | 
						|
# --------------------
 | 
						|
 | 
						|
def _cleanOldDomainJoins():
 | 
						|
    # stop sssd
 | 
						|
    logging.info("Stopping sssd")
 | 
						|
    if subprocess.call(["service", "sssd", "stop"]) != 0:
 | 
						|
        logging.error("Failed!")
 | 
						|
        return False
 | 
						|
 | 
						|
    # Clean old domain join data
 | 
						|
    logging.info("Deleting old kerberos tickets.")
 | 
						|
    subprocess.call(["kdestroy"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
						|
 | 
						|
    if not realm.leaveAll():
 | 
						|
        return False
 | 
						|
 | 
						|
    # delete krb5.keytab file, if existent
 | 
						|
    logging.info('Deleting krb5.keytab if it exists ... ')
 | 
						|
    if not fileHelper.deleteFile("/etc/krb5.keytab"):
 | 
						|
        return False
 | 
						|
 | 
						|
    # delete old CA Certificate
 | 
						|
    logging.info('Deleting old CA certificate if it exists ... ')
 | 
						|
    if not fileHelper.deleteFilesWithExtension("/var/lib/samba/private/tls", ".pem"):
 | 
						|
        return False
 | 
						|
 | 
						|
    # remove network.conf
 | 
						|
    logging.info(f"Deleting {constants.networkConfigFilePath} if exists ...")
 | 
						|
    if not fileHelper.deleteFile(constants.networkConfigFilePath):
 | 
						|
        return False
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _findDomain(domain=None):
 | 
						|
    logging.info("Trying to discover available domains...")
 | 
						|
    rc, availableDomains = realm.discoverDomains()
 | 
						|
    if not rc or len(availableDomains) < 1:
 | 
						|
        logging.error("Could not discover any domain!")
 | 
						|
        return False, None
 | 
						|
    
 | 
						|
    if domain == None:
 | 
						|
        domain = availableDomains[0]
 | 
						|
        logging.info(f"Using first discovered domain {domain}")
 | 
						|
    elif domain in availableDomains:
 | 
						|
        logging.info(f"Using domain {domain}")
 | 
						|
    else:
 | 
						|
        print("\n")
 | 
						|
        logging.error(f"Could not find domain {domain}!")
 | 
						|
        return False, None
 | 
						|
    
 | 
						|
    return True, domain
 | 
						|
 | 
						|
def _prepareNetworkConfiguration(domain):
 | 
						|
    logging.info("Preparing network configuration")
 | 
						|
    rc, domainConfig = realm.getDomainConfig(domain)
 | 
						|
    if not rc:
 | 
						|
        logging.error("Could not read domain configuration")
 | 
						|
        return False
 | 
						|
 | 
						|
    newNetworkConfig = {}
 | 
						|
    newNetworkConfig["serverHostname"] = domainConfig["domain-controller"]
 | 
						|
    newNetworkConfig["domain"] = domainConfig["domain-name"]
 | 
						|
    newNetworkConfig["realm"] = domainConfig["domain-name"].upper()
 | 
						|
 | 
						|
    config.writeNetworkConfig(newNetworkConfig)
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _preparePam():
 | 
						|
    # enable necessary pam modules
 | 
						|
    logging.info('Updating pam configuration ... ')
 | 
						|
    subprocess.call(['pam-auth-update', '--package', '--enable', 'libpam-mount', 'pwquality', 'sss', '--force'])
 | 
						|
    ## mkhomedir was injected in template not using pam-auth-update
 | 
						|
    subprocess.call(['pam-auth-update', '--package', '--remove', 'krb5', 'mkhomedir', '--force'])
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _prepareServices():
 | 
						|
    logging.info("Raloading systctl daemon")
 | 
						|
    subprocess.call(["systemctl", "daemon-reload"])
 | 
						|
 | 
						|
    logging.info('Enabling services:')
 | 
						|
    services = ['linuxmuster-linuxclient7', 'smbd', 'nmbd', 'sssd']
 | 
						|
    for service in services:
 | 
						|
        logging.info('* %s' % service)
 | 
						|
        subprocess.call(['systemctl','enable', service + '.service'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
						|
 | 
						|
    logging.info('Restarting services:')
 | 
						|
    services = ['smbd', 'nmbd', 'systemd-timesyncd']
 | 
						|
    for service in services:
 | 
						|
        logging.info('* %s' % service)
 | 
						|
        subprocess.call(['systemctl', 'restart' , service + '.service'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _installCaCertificate(domain, user):
 | 
						|
    logging.info('Installing server ca certificate ... ')
 | 
						|
 | 
						|
    # try to mount the share
 | 
						|
    rc, sysvolMountpoint = shares.getLocalSysvolPath()
 | 
						|
    if not rc:
 | 
						|
        logging.error("Failed to mount sysvol!")
 | 
						|
        return False
 | 
						|
 | 
						|
    cacertPath = f"{sysvolMountpoint}/{domain}/tls/cacert.pem"
 | 
						|
    cacertTargetPath = f"/var/lib/samba/private/tls/{domain}.pem"
 | 
						|
 | 
						|
    logging.info("Copying CA certificate from server to client!")
 | 
						|
    try:
 | 
						|
        Path(Path(cacertTargetPath).parent.absolute()).mkdir(parents=True, exist_ok=True)
 | 
						|
        shutil.copyfile(cacertPath, cacertTargetPath)
 | 
						|
    except Exception as e:
 | 
						|
        logging.error("Failed!")
 | 
						|
        logging.exception(e)
 | 
						|
        return False
 | 
						|
 | 
						|
    # make sure the file was successfully copied
 | 
						|
    if not os.path.isfile(cacertTargetPath):
 | 
						|
        logging.error('Failed to copy over CA certificate!')
 | 
						|
        return False
 | 
						|
 | 
						|
    # unmount sysvol
 | 
						|
    shares.unmountAllSharesOfUser(computer.krbHostName())
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _adjustSssdConfiguration(domain):
 | 
						|
    logging.info("Adjusting sssd.conf")
 | 
						|
 | 
						|
    sssdConfigFilePath = '/etc/sssd/sssd.conf'
 | 
						|
    sssdConfig = configparser.ConfigParser(interpolation=None)
 | 
						|
 | 
						|
    sssdConfig.read(sssdConfigFilePath)
 | 
						|
    # accept usernames without domain
 | 
						|
    sssdConfig[f"domain/{domain}"]["use_fully_qualified_names"] = "False"
 | 
						|
 | 
						|
    # override homedir
 | 
						|
    sssdConfig[f"domain/{domain}"]["override_homedir"] = "/home/%u"
 | 
						|
 | 
						|
    # Don't validate KVNO! Otherwise the Login will fail when the KVNO stored 
 | 
						|
    # in /etc/krb5.keytab does not match the one in the AD (msDS-KeyVersionNumber)
 | 
						|
    sssdConfig[f"domain/{domain}"]["krb5_validate"] = "False"
 | 
						|
 | 
						|
    sssdConfig[f"domain/{domain}"]["ad_gpo_access_control"] = "permissive"
 | 
						|
    sssdConfig[f"domain/{domain}"]["ad_gpo_ignore_unreadable"] = "True"
 | 
						|
 | 
						|
    # Don't renew the machine password, as this will break the domain join
 | 
						|
    # See: https://github.com/linuxmuster/linuxmuster-linuxclient7/issues/27
 | 
						|
    sssdConfig[f"domain/{domain}"]["ad_maximum_machine_account_password_age"] = "0"
 | 
						|
 | 
						|
    # Make sure usernames are not case sensitive
 | 
						|
    sssdConfig[f"domain/{domain}"]["case_sensitive"] = "False"
 | 
						|
 | 
						|
    try:
 | 
						|
        logging.info("Writing new Configuration")
 | 
						|
        with open(sssdConfigFilePath, 'w') as sssdConfigFile:
 | 
						|
            sssdConfig.write(sssdConfigFile)
 | 
						|
 | 
						|
    except Exception as e:
 | 
						|
        logging.error("Failed!")
 | 
						|
        logging.exception(e)
 | 
						|
        return False
 | 
						|
 | 
						|
    logging.info("Restarting sssd")
 | 
						|
    if subprocess.call(["service", "sssd", "restart"]) != 0:
 | 
						|
        logging.error("Failed!")
 | 
						|
        return False
 | 
						|
 | 
						|
    return True
 | 
						|
 | 
						|
def _deleteObsoleteFiles():
 | 
						|
    
 | 
						|
    # files
 | 
						|
    logging.info("Deleting obsolete files")
 | 
						|
 | 
						|
    for obsoleteFile in constants.obsoleteFiles:
 | 
						|
        logging.info(f"* {obsoleteFile}")
 | 
						|
        fileHelper.deleteFile(obsoleteFile)
 | 
						|
 | 
						|
    # directories
 | 
						|
    logging.info("Deleting obsolete directories")
 | 
						|
 | 
						|
    for obsoleteDirectory in constants.obsoleteDirectories:
 | 
						|
        logging.info(f"* {obsoleteDirectory}")
 | 
						|
        fileHelper.deleteDirectory(obsoleteDirectory)
 | 
						|
 | 
						|
    return True |