From 2fe52816e47f7fafcbe7df4e5b11462c88af5e15 Mon Sep 17 00:00:00 2001 From: Raphael Dannecker Date: Wed, 16 Aug 2023 12:17:17 +0200 Subject: [PATCH] install printer based on GPO --- lmn-desktop.yml | 1 + roles/lmn_printer/defaults/main.yml | 2 + roles/lmn_printer/files/90-lmn-sudotools | 4 + .../files/linuxmusterLinuxclient7/__init__.py | 3 + .../files/linuxmusterLinuxclient7/computer.py | 57 +++ .../files/linuxmusterLinuxclient7/config.py | 136 +++++++ .../linuxmusterLinuxclient7/constants.py | 46 +++ .../linuxmusterLinuxclient7/environment.py | 60 +++ .../linuxmusterLinuxclient7/fileHelper.py | 133 +++++++ .../files/linuxmusterLinuxclient7/gpo.py | 291 ++++++++++++++ .../files/linuxmusterLinuxclient7/gpo.py.orig | 290 ++++++++++++++ .../files/linuxmusterLinuxclient7/hooks.py | 219 +++++++++++ .../linuxmusterLinuxclient7/imageHelper.py | 220 +++++++++++ .../files/linuxmusterLinuxclient7/keytab.py | 52 +++ .../linuxmusterLinuxclient7/ldapHelper.py | 148 +++++++ .../localUserHelper.py | 19 + .../files/linuxmusterLinuxclient7/logging.py | 130 +++++++ .../files/linuxmusterLinuxclient7/printers.py | 129 +++++++ .../files/linuxmusterLinuxclient7/realm.py | 195 ++++++++++ .../files/linuxmusterLinuxclient7/setup.py | 364 ++++++++++++++++++ .../files/linuxmusterLinuxclient7/shares.py | 256 ++++++++++++ .../linuxmusterLinuxclient7/templates.py | 128 ++++++ .../files/linuxmusterLinuxclient7/user.py | 158 ++++++++ roles/lmn_printer/files/lmn-printer.sh | 1 + roles/lmn_printer/files/onLogin | 33 ++ roles/lmn_printer/files/onLogout | 38 ++ roles/lmn_printer/files/rmlpr.service | 6 + roles/lmn_printer/files/rmlpr.timer | 8 + roles/lmn_printer/files/scripts/sudoTools | 50 +++ roles/lmn_printer/tasks/main.yml | 102 +++++ roles/lmn_printer/templates/network.conf.j2 | 5 + 31 files changed, 3284 insertions(+) create mode 100644 roles/lmn_printer/defaults/main.yml create mode 100644 roles/lmn_printer/files/90-lmn-sudotools create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/__init__.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/config.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/environment.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/localUserHelper.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py create mode 100644 roles/lmn_printer/files/linuxmusterLinuxclient7/user.py create mode 100644 roles/lmn_printer/files/lmn-printer.sh create mode 100644 roles/lmn_printer/files/onLogin create mode 100644 roles/lmn_printer/files/onLogout create mode 100644 roles/lmn_printer/files/rmlpr.service create mode 100644 roles/lmn_printer/files/rmlpr.timer create mode 100755 roles/lmn_printer/files/scripts/sudoTools create mode 100644 roles/lmn_printer/tasks/main.yml create mode 100644 roles/lmn_printer/templates/network.conf.j2 diff --git a/lmn-desktop.yml b/lmn-desktop.yml index 1112ed1..3c30831 100644 --- a/lmn-desktop.yml +++ b/lmn-desktop.yml @@ -47,6 +47,7 @@ - lmn_mount - lmn_kde - lmn_vm + - lmn_printer - kerberize tasks: diff --git a/roles/lmn_printer/defaults/main.yml b/roles/lmn_printer/defaults/main.yml new file mode 100644 index 0000000..cf3a823 --- /dev/null +++ b/roles/lmn_printer/defaults/main.yml @@ -0,0 +1,2 @@ +smb_server: "server" +smb_share: "default-school/" diff --git a/roles/lmn_printer/files/90-lmn-sudotools b/roles/lmn_printer/files/90-lmn-sudotools new file mode 100644 index 0000000..1c82b4d --- /dev/null +++ b/roles/lmn_printer/files/90-lmn-sudotools @@ -0,0 +1,4 @@ +%examusers ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools +%role-student ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools +%role-teacher ALL=(root) NOPASSWD: /usr/share/linuxmuster-linuxclient7/scripts/sudoTools + diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/__init__.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/__init__.py new file mode 100644 index 0000000..bf319b7 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/__init__.py @@ -0,0 +1,3 @@ +# +# linuxmuster-linuxclient7 is a library for use with Linuxmuster.net +# \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py new file mode 100644 index 0000000..701ab81 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/computer.py @@ -0,0 +1,57 @@ +import socket +from linuxmusterLinuxclient7 import logging, ldapHelper, realm, localUserHelper + +def hostname(): + """ + Get the hostname of the computer + + :return: The hostname + :rtype: str + """ + return socket.gethostname().split('.', 1)[0] + +def krbHostName(): + """ + Get the krb hostname, eg. `COMPUTER01$` + + :return: The krb hostname + :rtype: str + """ + return hostname().upper() + "$" + +def readAttributes(): + """ + Read all ldap attributes of the cumputer + + :return: Tuple (success, dict of attributes) + :rtype: tuple + """ + return ldapHelper.searchOne("(sAMAccountName={}$)".format(hostname())) + +def isInGroup(groupName): + """ + Check if the computer is part of an ldap group + + :param groupName: The name of the group to check + :type grouName: str + :return: True or False + :rtype: bool + """ + rc, groups = localUserHelper.getGroupsOfLocalUser(krbHostName()) + if not rc: + return False + + return groupName in groups + +def isInAD(): + """ + Check if the computer is joined to an AD + + :return: True or False + :rtype: bool + """ + rc, groups = localUserHelper.getGroupsOfLocalUser(krbHostName()) + if not rc: + return False + + return "domain computers" in groups \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/config.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/config.py new file mode 100644 index 0000000..bbfbaf9 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/config.py @@ -0,0 +1,136 @@ +import configparser, re +from linuxmusterLinuxclient7 import logging, constants + +def network(): + """ + Get the network configuration in `/etc/linuxmuster-linuxclient7/network.conf` + + :return: Tuple (success, dict of keys) + :rtype: tuple + """ + rc, rawNetworkConfig = _readNetworkConfig() + if not rc: + return False, None + + if not _checkNetworkConfigVersion(rawNetworkConfig)[0]: + return False, None + + networkConfig = {} + + try: + networkConfig["serverHostname"] = rawNetworkConfig["serverHostname"] + networkConfig["domain"] = rawNetworkConfig["domain"] + networkConfig["realm"] = rawNetworkConfig["realm"] + except KeyError as e: + logging.error("Error when reading network.conf (2)") + logging.exception(e) + return False, None + + return True, networkConfig + +def writeNetworkConfig(newNetworkConfig): + """ + Write the network configuration in `/etc/linuxmuster-linuxclient7/network.conf` + + :param newNetworkConfig: The new config + :type newNetworkConfig: dict + :return: True or False + :rtype: bool + """ + networkConfig = configparser.ConfigParser(interpolation=None) + + try: + networkConfig["network"] = {} + networkConfig["network"]["version"] = str(_networkConfigVersion()) + networkConfig["network"]["serverHostname"] = newNetworkConfig["serverHostname"] + networkConfig["network"]["domain"] = newNetworkConfig["domain"] + networkConfig["network"]["realm"] = newNetworkConfig["realm"] + except Exception as e: + logging.error("Error when preprocessing new network configuration!") + logging.exception(e) + return False + + try: + logging.info("Writing new network Configuration") + with open(constants.networkConfigFilePath, 'w') as networkConfigFile: + networkConfig.write(networkConfigFile) + + except Exception as e: + logging.error("Failed!") + logging.exception(e) + return False + + return True + +def upgrade(): + """ + Upgrade the format of the network configuration in `/etc/linuxmuster-linuxclient7/network.conf` + This is done automatically on package upgrades. + + :return: True or False + :rtype: bool + """ + return _upgradeNetworkConfig() + +# -------------------- +# - Helper functions - +# -------------------- + +def _readNetworkConfig(): + configParser = configparser.ConfigParser() + configParser.read(constants.networkConfigFilePath) + try: + rawNetworkConfig = configParser["network"] + return True, rawNetworkConfig + except KeyError as e: + logging.error("Error when reading network.conf (1)") + logging.exception(e) + return False, None + return configParser + +def _networkConfigVersion(): + return 1 + +def _checkNetworkConfigVersion(rawNetworkConfig): + try: + networkConfigVersion = int(rawNetworkConfig["version"]) + except KeyError as e: + logging.warning("The network.conf version could not be identified, assuming 0") + networkConfigVersion = 0 + + if networkConfigVersion != _networkConfigVersion(): + logging.warning("The network.conf Version is a mismatch!") + return False, networkConfigVersion + + return True, networkConfigVersion + +def _upgradeNetworkConfig(): + logging.info("Upgrading network config.") + rc, rawNetworkConfig = _readNetworkConfig() + if not rc: + return False + + rc, networkConfigVersion = _checkNetworkConfigVersion(rawNetworkConfig) + if rc: + logging.info("No need to upgrade, already up-to-date.") + return True + + logging.info("Upgrading network config from {0} to {1}".format(networkConfigVersion, _networkConfigVersion())) + + if networkConfigVersion > _networkConfigVersion(): + logging.error("Cannot upgrade from a newer version to an older one!") + return False + + try: + if networkConfigVersion == 0: + newNetworkConfig = {} + newNetworkConfig["serverHostname"] = rawNetworkConfig["serverHostname"] + "." + rawNetworkConfig["domain"] + newNetworkConfig["domain"] = rawNetworkConfig["domain"] + newNetworkConfig["realm"] = rawNetworkConfig["domain"].upper() + return writeNetworkConfig(newNetworkConfig) + except Exception as e: + logging.error("Error when upgrading network config!") + logging.exception(e) + return False + + return True \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py new file mode 100644 index 0000000..34d8243 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/constants.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +templateUser = "linuxadmin" +userTemplateDir = "/home/" + templateUser +defaultDomainAdminUser = "global-admin" + +# {} will be substituted for the username +shareMountBasepath = "/home/{}/media" +hiddenShareMountBasepath = "/srv/samba/{}" +machineAccountSysvolMountPath = "/var/lib/samba/sysvol" + +etcBaseDir = "/etc/linuxmuster-linuxclient7" +shareBaseDir = "/usr/share/linuxmuster-linuxclient7" +configFileTemplateDir = shareBaseDir + "/templates" +scriptDir = shareBaseDir + "/scripts" + +networkConfigFilePath = etcBaseDir + "/network.conf" +# {} will be substituted for the username +tmpEnvironmentFilePath = "/home/{}/.linuxmuster-linuxclient7-environment.sh" + +notTemplatableFiles = ["/etc/sssd/sssd.conf", "/etc/linuxmuster-linuxclient7/network.conf"] + +# cleanup +obsoleteFiles = [ + "/etc/profile.d/99-linuxmuster.sh", + "/etc/sudoers.d/linuxmuster", + "/etc/profile.d/linuxmuster-proxy.sh", + "/etc/bash_completion.d/99-linuxmuster-client-adsso.sh", + "/etc/profile.d/99-linuxmuster-client-adsso.sh", + "/etc/sudoers.d/linuxmuster-client-adsso", + "/usr/sbin/linuxmuster-client-adsso", + "/usr/sbin/linuxmuster-client-adsso-print-logs", + "/etc/systemd/system/linuxmuster-client-adsso.service", + "{}/.config/autostart/linuxmuster-client-adsso-autostart.desktop".format(userTemplateDir), + "/etc/cups/client.conf", + "/usr/share/linuxmuster-linuxclient7/templates/linuxmuster-client-adsso.service", + "/usr/share/linuxmuster-linuxclient7/templates/linuxmuster-client-adsso-autostart.desktop", + "/etc/security/pam_mount.conf.xml", + "{}/pam_mount.conf.xml".format(configFileTemplateDir) +] + +obsoleteDirectories = [ + "/etc/linuxmuster-client", + "/etc/linuxmuster-client-adsso", + "/usr/share/linuxmuster-client-adsso" +] diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/environment.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/environment.py new file mode 100644 index 0000000..e25b622 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/environment.py @@ -0,0 +1,60 @@ +import os +from linuxmusterLinuxclient7 import constants, user, logging + +def export(keyValuePair): + """ + Export an environment variable + + :param keyValuePair: Key value pair in format `key=value` + :type keyValuePait: str + :return: True or False + :rtype: bool + """ + logging.debug("Saving export '{}' to tmp file".format(keyValuePair)) + + envList = keyValuePair.split("=") + if len(envList) == 2: + os.putenv(envList[0], envList[1]) + + return _appendToTmpEnvFile("export", keyValuePair) + +def unset(key): + """ + Unset a previously exported environment variable + + :param key: The key to unset + :type key: str + :return: True or False + :rtype: bool + """ + logging.debug("Saving unset '{}' to tmp file".format(key)) + return _appendToTmpEnvFile("unset", key) + +# -------------------- +# - Helper functions - +# -------------------- + +def _isApplicable(): + if not user.isInAD(): + logging.error("Modifying environment variables of non-AD users is not supported by lmn-export and lmn-unset!") + return False + elif "LinuxmusterLinuxclient7EnvFixActive" not in os.environ or os.environ["LinuxmusterLinuxclient7EnvFixActive"] != "1": + logging.error("lmn-export and lmn-unset may only be used inside of linuxmuster-linuxclient7 hooks!") + return False + else: + return True + +def _appendToTmpEnvFile(mode, keyValuePair): + if not _isApplicable(): + return False + + tmpEnvironmentFilePath = constants.tmpEnvironmentFilePath.format(user.username()) + fileOpenMode = "a" if os.path.exists(tmpEnvironmentFilePath) else "w" + + try: + with open(tmpEnvironmentFilePath, fileOpenMode) as tmpEnvironmentFile: + tmpEnvironmentFile.write("\n{0} '{1}'".format(mode, keyValuePair)) + return True + except Exception as e: + logging.exception(e) + return False \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py new file mode 100644 index 0000000..ad9b55e --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/fileHelper.py @@ -0,0 +1,133 @@ +import os, shutil +from linuxmusterLinuxclient7 import logging + +def removeLinesInFileContainingString(filePath, forbiddenStrings): + """ + Remove all lines containing a given string form a file. + + :param filePath: The path to the file + :type filePath: str + :param forbiddenStrings: The string to search for + :type forbiddenStrings: str + :return: True on success, False otherwise + :rtype: bool + """ + if not isinstance(forbiddenStrings, list): + forbiddenStrings = [forbiddenStrings] + + try: + with open(filePath, "r") as originalFile: + originalContents = originalFile.read() + except Exception as e: + logging.exception(e) + logging.warning("Could not read contents of original file") + return False + + newContents = "" + for line in originalContents.split("\n"): + lineIsClean = True + for forbiddenString in forbiddenStrings: + lineIsClean = lineIsClean and not forbiddenString in line + + if lineIsClean : + newContents += line + "\n" + + try: + with open(filePath, "w") as originalFile: + originalFile.write(newContents) + except Exception as e: + logging.exception(e) + logging.warning("Could not write new contents to original file") + return False + + return True + +def deleteFile(filePath): + """ + Delete a file + + :param filePath: The path of the file + :type filePath: str + :return: True on success, False otherwise + :rtype: bool + """ + try: + if os.path.exists(filePath): + os.unlink(filePath) + return True + except Exception as e: + logging.error("Failed!") + logging.exception(e) + return False + +def deleteFilesWithExtension(directory, extension): + """ + Delete all files with a given extension in a given directory. + + :param directory: The path of the directory + :type directory: str + :param extension: The file extension + :type extension: str + :return: True on success, False otherwise + :rtype: bool + """ + if directory.endswith("/"): + directory = directory[:-1] + + if not os.path.exists(directory): + return True + + existingFiles=os.listdir(directory) + + for file in existingFiles: + if file.endswith(extension): + logging.info("* Deleting {}".format(file)) + if not deleteFile("{}/{}".format(directory, file)): + logging.error("Failed!") + return False + + return True + +def deleteDirectory(directory): + """ + Recoursively delete a directory. + + :param directory: The path of the directory + :type directory: bool + :return: True on success, False otherwise + :rtype: bool + """ + try: + shutil.rmtree(directory) + except: + return False + return True + +def deleteAllInDirectory(directory): + """ + Delete all files in a given directory + + :param directory: The path of the directory + :type directory: str + :return: True on success, False otherwise + :rtype: bool + """ + + if directory.endswith("/"): + directory = directory[:-1] + + if not os.path.exists(directory): + return True + + existingFiles=os.listdir(directory) + for file in existingFiles: + fullFilePath = "{}/{}".format(directory, file) + if os.path.isdir(fullFilePath): + rc = deleteDirectory(fullFilePath) + else: + rc = deleteFile(fullFilePath) + if not rc: + logging.error("Failed!") + return False + + return True \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py new file mode 100644 index 0000000..43d08a8 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py @@ -0,0 +1,291 @@ + # Order of parsing: (overwriting each other) + # 1. Local (does not apply) + # 2. Site (does not apply) + # 3. Domain + # 4. OUs from top to bottom +import ldap, ldap.sasl, re, os.path +import xml.etree.ElementTree as ElementTree +from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, computer, printers + +def processAllPolicies(): + """ + Process all applicable policies (equivalent to gpupdate on windows) + + :return: True on success, False otherwise + :rtype: bool + """ + rc, policyDnList = _findApplicablePolicies() + if not rc: + logging.fatal("* Error when loading applicable GPOs! Shares and printers will not work.") + return False + + for policyDn in policyDnList: + _parsePolicy(policyDn) + +# -------------------- +# - Helper functions - +# -------------------- + +def _parseGplinkSring(string): + # a gPLink strink looks like this: + # [LDAP://;][LDAP://;][...] + # The ragex matches and in two separate groups + # Note: "LDAP://" is matched as .*:// to prevent issues when the capitalization changes + pattern = re.compile("\\[.*:\\/\\/([^\\]]+)\\;([0-9]+)\\]") + + return pattern.findall(string) + +def _extractOUsFromDN(dn): + # NOT finished! + pattern = re.compile("OU=([^,]+),") + + ouList = pattern.findall(dn) + # We need to parse from top to bottom + ouList.reverse() + return ouList + +def _findApplicablePolicies(): + + policyDnList = [] + + """ Do this later! + # 1. Domain + rc, domainAdObject = ldapHelper.searchOne("(distinguishedName={})".format(ldapHelper.baseDn())) + + if not rc: + return False, None + + policyDNs.extend(_parseGplinkSring(domainAdObject["gPLink"])) + + # 2. OU policies from top to bottom + rc, userAdObject = ldapHelper.searchOne("(sAMAccountName={})".format(user.username())) + + if not rc: + return False, None + + print(userAdObject["distinguishedName"]) + """ + + # For now, just parse policy sophomorix:school: + rc, schoolName = user.school() + if not rc: + return False, None + + policyName = "sophomorix:school:{}".format(schoolName) + + # find policy + rc, policyAdObject = ldapHelper.searchOne("(displayName={})".format(policyName)) + if not rc: + return False, None + + policyDnList.append((policyAdObject["distinguishedName"], 0)) + + return True, policyDnList + +def _parsePolicy(policyDn): + logging.info("=== Parsing policy [{0};{1}] ===".format(policyDn[0], policyDn[1])) + + # Check if the policy is disabled + if policyDn[1] == 1: + logging.info("===> Policy is disabled! ===") + return True + + # Find policy in AD + rc, policyAdObject = ldapHelper.searchOne("(distinguishedName={})".format(policyDn[0])) + if not rc: + logging.error("===> Could not find poilcy in AD! ===") + return False, None + + # mount the share the policy is on (probaply already mounted, just to be sure) + rc, localPolicyPath = shares.getMountpointOfRemotePath(policyAdObject["gPCFileSysPath"], hiddenShare = True, autoMount = True) + if not rc: + logging.error("===> Could not mount path of poilcy! ===") + return False, None + + try: + # parse drives + # Skip Drive Policys (fvs change) + #_processDrivesPolicy(localPolicyPath) + # parse printers + _processPrintersPolicy(localPolicyPath) + except Exception as e: + logging.error("An error occured when parsing policy!") + logging.exception(e) + + logging.info("===> Parsed policy [{0};{1}] ===".format(policyDn[0], policyDn[1])) + +def _parseXmlFilters(filtersXmlNode): + if not filtersXmlNode.tag == "Filters": + logging.warning("Tried to parse a non-filter node as a filter!") + return [] + + filters = [] + + for xmlFilter in filtersXmlNode: + if xmlFilter.tag == "FilterGroup": + filters.append({ + "name": xmlFilter.attrib["name"].split("\\")[1], + "bool": xmlFilter.attrib["bool"], + "userContext": xmlFilter.attrib["userContext"], + # userContext defines if the filter applies in user or computer context + "type": xmlFilter.tag + }) + + return filters + +def _processFilters(policies): + filteredPolicies = [] + + for policy in policies: + if not len(policy["filters"]) > 0: + filteredPolicies.append(policy) + else: + filtersPassed = True + for filter in policy["filters"]: + logging.debug("Testing filter: {}".format(filter)) + # TODO: check for AND and OR + if filter["bool"] == "AND": + filtersPassed = filtersPassed and _processFilter(filter) + elif filter["bool"] == "OR": + filtersPassed = filtersPassed or _processFilter(filter) + else: + logging.warning("Unknown boolean operation: {}! Cannot process filter!".format(filter["bool"])) + + if filtersPassed: + filteredPolicies.append(policy) + + return filteredPolicies + +def _processFilter(filter): + if filter["type"] == "FilterGroup": + if filter["userContext"] == "1": + return user.isInGroup(filter["name"]) + elif filter["userContext"] == "0": + return computer.isInGroup(filter["name"]) + + return False + +def _parseXmlPolicy(policyFile): + if not os.path.isfile(policyFile): + logging.warning("==> XML policy file not found! ==") + return False, None + + try: + tree = ElementTree.parse(policyFile) + return True, tree + except Exception as e: + logging.exception(e) + logging.error("==> Error while reading XML policy file! ==") + return False, None + +def _processDrivesPolicy(policyBasepath): + logging.info("== Parsing a drive policy! ==") + policyFile = "{}/User/Preferences/Drives/Drives.xml".format(policyBasepath) + shareList = [] + + rc, tree = _parseXmlPolicy(policyFile) + + if not rc: + logging.error("==> Error while reading Drives policy file, skipping! ==") + return + + xmlDrives = tree.getroot() + + if not xmlDrives.tag == "Drives": + logging.warning("==> Drive policy xml File is of invalid format, skipping! ==") + return + + for xmlDrive in xmlDrives: + + if xmlDrive.tag != "Drive" or ("disabled" in xmlDrive.attrib and xmlDrive.attrib["disabled"] == "1"): + continue + + drive = {} + drive["filters"] = [] + for xmlDriveProperty in xmlDrive: + if xmlDriveProperty.tag == "Properties": + try: + drive["label"] = xmlDriveProperty.attrib["label"] + drive["letter"] = xmlDriveProperty.attrib["letter"] + drive["path"] = xmlDriveProperty.attrib["path"] + drive["useLetter"] = xmlDriveProperty.attrib["useLetter"] + except Exception as e: + logging.warning("Exception when parsing a drive policy XML file") + logging.exception(e) + continue + + if xmlDriveProperty.tag == "Filters": + drive["filters"] = _parseXmlFilters(xmlDriveProperty) + + shareList.append(drive) + + shareList = _processFilters(shareList) + + logging.info("Found shares:") + for drive in shareList: + logging.info("* {:10}| {:5}| {:40}| {:5}".format(drive["label"], drive["letter"], drive["path"], drive["useLetter"])) + + for drive in shareList: + if drive["useLetter"] == "1": + shareName = f"{drive['label']} ({drive['letter']}:)" + else: + shareName = drive["label"] + shares.mountShare(drive["path"], shareName=shareName) + + logging.info("==> Successfully parsed a drive policy! ==") + +def _processPrintersPolicy(policyBasepath): + logging.info("== Parsing a printer policy! ==") + policyFile = "{}/User/Preferences/Printers/Printers.xml".format(policyBasepath) + printerList = [] + # test + rc, tree = _parseXmlPolicy(policyFile) + + if not rc: + logging.error("==> Error while reading Printer policy file, skipping! ==") + return + + xmlPrinters = tree.getroot() + + if not xmlPrinters.tag == "Printers": + logging.warning("==> Printer policy xml File is of invalid format, skipping! ==") + return + + for xmlPrinter in xmlPrinters: + + if xmlPrinter.tag != "SharedPrinter" or ("disabled" in xmlPrinter.attrib and xmlPrinter.attrib["disabled"] == "1"): + continue + + printer = {} + printer["filters"] = [] + + try: + printer["name"] = xmlPrinter.attrib["name"] + except Exception as e: + logging.warning("Exception when reading a printer name from a printer policy XML file") + logging.exception(e) + + for xmlPrinterProperty in xmlPrinter: + if xmlPrinterProperty.tag == "Properties": + try: + rc, printerUrl = printers.translateSambaToIpp(xmlPrinterProperty.attrib["path"]) + if rc: + printer["path"] = printerUrl + except Exception as e: + logging.warning("Exception when parsing a printer policy XML file") + logging.exception(e) + continue + + if xmlPrinterProperty.tag == "Filters": + printer["filters"] = _parseXmlFilters(xmlPrinterProperty) + + printerList.append(printer) + + printerList = _processFilters(printerList) + + logging.info("Found printers:") + for printer in printerList: + logging.info("* {0}\t\t| {1}\t| {2}".format(printer["name"], printer["path"], printer["filters"])) + printers.installPrinter(printer["path"], printer["name"]) + + logging.info("==> Successfully parsed a printer policy! ==") diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig b/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig new file mode 100644 index 0000000..20f10ba --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/gpo.py.orig @@ -0,0 +1,290 @@ + # Order of parsing: (overwriting each other) + # 1. Local (does not apply) + # 2. Site (does not apply) + # 3. Domain + # 4. OUs from top to bottom +import ldap, ldap.sasl, re, os.path +import xml.etree.ElementTree as ElementTree +from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, computer, printers + +def processAllPolicies(): + """ + Process all applicable policies (equivalent to gpupdate on windows) + + :return: True on success, False otherwise + :rtype: bool + """ + rc, policyDnList = _findApplicablePolicies() + if not rc: + logging.fatal("* Error when loading applicable GPOs! Shares and printers will not work.") + return False + + for policyDn in policyDnList: + _parsePolicy(policyDn) + +# -------------------- +# - Helper functions - +# -------------------- + +def _parseGplinkSring(string): + # a gPLink strink looks like this: + # [LDAP://;][LDAP://;][...] + # The ragex matches and in two separate groups + # Note: "LDAP://" is matched as .*:// to prevent issues when the capitalization changes + pattern = re.compile("\\[.*:\\/\\/([^\\]]+)\\;([0-9]+)\\]") + + return pattern.findall(string) + +def _extractOUsFromDN(dn): + # NOT finished! + pattern = re.compile("OU=([^,]+),") + + ouList = pattern.findall(dn) + # We need to parse from top to bottom + ouList.reverse() + return ouList + +def _findApplicablePolicies(): + + policyDnList = [] + + """ Do this later! + # 1. Domain + rc, domainAdObject = ldapHelper.searchOne("(distinguishedName={})".format(ldapHelper.baseDn())) + + if not rc: + return False, None + + policyDNs.extend(_parseGplinkSring(domainAdObject["gPLink"])) + + # 2. OU policies from top to bottom + rc, userAdObject = ldapHelper.searchOne("(sAMAccountName={})".format(user.username())) + + if not rc: + return False, None + + print(userAdObject["distinguishedName"]) + """ + + # For now, just parse policy sophomorix:school: + rc, schoolName = user.school() + if not rc: + return False, None + + policyName = "sophomorix:school:{}".format(schoolName) + + # find policy + rc, policyAdObject = ldapHelper.searchOne("(displayName={})".format(policyName)) + if not rc: + return False, None + + policyDnList.append((policyAdObject["distinguishedName"], 0)) + + return True, policyDnList + +def _parsePolicy(policyDn): + logging.info("=== Parsing policy [{0};{1}] ===".format(policyDn[0], policyDn[1])) + + # Check if the policy is disabled + if policyDn[1] == 1: + logging.info("===> Policy is disabled! ===") + return True + + # Find policy in AD + rc, policyAdObject = ldapHelper.searchOne("(distinguishedName={})".format(policyDn[0])) + if not rc: + logging.error("===> Could not find poilcy in AD! ===") + return False, None + + # mount the share the policy is on (probaply already mounted, just to be sure) + rc, localPolicyPath = shares.getMountpointOfRemotePath(policyAdObject["gPCFileSysPath"], hiddenShare = True, autoMount = True) + if not rc: + logging.error("===> Could not mount path of poilcy! ===") + return False, None + + try: + # parse drives + _processDrivesPolicy(localPolicyPath) + # parse printers + _processPrintersPolicy(localPolicyPath) + except Exception as e: + logging.error("An error occured when parsing policy!") + logging.exception(e) + + logging.info("===> Parsed policy [{0};{1}] ===".format(policyDn[0], policyDn[1])) + +def _parseXmlFilters(filtersXmlNode): + if not filtersXmlNode.tag == "Filters": + logging.warning("Tried to parse a non-filter node as a filter!") + return [] + + filters = [] + + for xmlFilter in filtersXmlNode: + if xmlFilter.tag == "FilterGroup": + filters.append({ + "name": xmlFilter.attrib["name"].split("\\")[1], + "bool": xmlFilter.attrib["bool"], + "userContext": xmlFilter.attrib["userContext"], + # userContext defines if the filter applies in user or computer context + "type": xmlFilter.tag + }) + + return filters + +def _processFilters(policies): + filteredPolicies = [] + + for policy in policies: + if not len(policy["filters"]) > 0: + filteredPolicies.append(policy) + else: + filtersPassed = True + for filter in policy["filters"]: + logging.debug("Testing filter: {}".format(filter)) + # TODO: check for AND and OR + if filter["bool"] == "AND": + filtersPassed = filtersPassed and _processFilter(filter) + elif filter["bool"] == "OR": + filtersPassed = filtersPassed or _processFilter(filter) + else: + logging.warning("Unknown boolean operation: {}! Cannot process filter!".format(filter["bool"])) + + if filtersPassed: + filteredPolicies.append(policy) + + return filteredPolicies + +def _processFilter(filter): + if filter["type"] == "FilterGroup": + if filter["userContext"] == "1": + return user.isInGroup(filter["name"]) + elif filter["userContext"] == "0": + return computer.isInGroup(filter["name"]) + + return False + +def _parseXmlPolicy(policyFile): + if not os.path.isfile(policyFile): + logging.warning("==> XML policy file not found! ==") + return False, None + + try: + tree = ElementTree.parse(policyFile) + return True, tree + except Exception as e: + logging.exception(e) + logging.error("==> Error while reading XML policy file! ==") + return False, None + +def _processDrivesPolicy(policyBasepath): + logging.info("== Parsing a drive policy! ==") + policyFile = "{}/User/Preferences/Drives/Drives.xml".format(policyBasepath) + shareList = [] + + rc, tree = _parseXmlPolicy(policyFile) + + if not rc: + logging.error("==> Error while reading Drives policy file, skipping! ==") + return + + xmlDrives = tree.getroot() + + if not xmlDrives.tag == "Drives": + logging.warning("==> Drive policy xml File is of invalid format, skipping! ==") + return + + for xmlDrive in xmlDrives: + + if xmlDrive.tag != "Drive" or ("disabled" in xmlDrive.attrib and xmlDrive.attrib["disabled"] == "1"): + continue + + drive = {} + drive["filters"] = [] + for xmlDriveProperty in xmlDrive: + if xmlDriveProperty.tag == "Properties": + try: + drive["label"] = xmlDriveProperty.attrib["label"] + drive["letter"] = xmlDriveProperty.attrib["letter"] + drive["path"] = xmlDriveProperty.attrib["path"] + drive["useLetter"] = xmlDriveProperty.attrib["useLetter"] + except Exception as e: + logging.warning("Exception when parsing a drive policy XML file") + logging.exception(e) + continue + + if xmlDriveProperty.tag == "Filters": + drive["filters"] = _parseXmlFilters(xmlDriveProperty) + + shareList.append(drive) + + shareList = _processFilters(shareList) + + logging.info("Found shares:") + for drive in shareList: + logging.info("* {:10}| {:5}| {:40}| {:5}".format(drive["label"], drive["letter"], drive["path"], drive["useLetter"])) + + for drive in shareList: + if drive["useLetter"] == "1": + shareName = f"{drive['label']} ({drive['letter']}:)" + else: + shareName = drive["label"] + shares.mountShare(drive["path"], shareName=shareName) + + logging.info("==> Successfully parsed a drive policy! ==") + +def _processPrintersPolicy(policyBasepath): + logging.info("== Parsing a printer policy! ==") + policyFile = "{}/User/Preferences/Printers/Printers.xml".format(policyBasepath) + printerList = [] + # test + rc, tree = _parseXmlPolicy(policyFile) + + if not rc: + logging.error("==> Error while reading Printer policy file, skipping! ==") + return + + xmlPrinters = tree.getroot() + + if not xmlPrinters.tag == "Printers": + logging.warning("==> Printer policy xml File is of invalid format, skipping! ==") + return + + for xmlPrinter in xmlPrinters: + + if xmlPrinter.tag != "SharedPrinter" or ("disabled" in xmlPrinter.attrib and xmlPrinter.attrib["disabled"] == "1"): + continue + + printer = {} + printer["filters"] = [] + + try: + printer["name"] = xmlPrinter.attrib["name"] + except Exception as e: + logging.warning("Exception when reading a printer name from a printer policy XML file") + logging.exception(e) + + for xmlPrinterProperty in xmlPrinter: + if xmlPrinterProperty.tag == "Properties": + try: + rc, printerUrl = printers.translateSambaToIpp(xmlPrinterProperty.attrib["path"]) + if rc: + printer["path"] = printerUrl + except Exception as e: + logging.warning("Exception when parsing a printer policy XML file") + logging.exception(e) + continue + + if xmlPrinterProperty.tag == "Filters": + printer["filters"] = _parseXmlFilters(xmlPrinterProperty) + + printerList.append(printer) + + printerList = _processFilters(printerList) + + logging.info("Found printers:") + for printer in printerList: + logging.info("* {0}\t\t| {1}\t| {2}".format(printer["name"], printer["path"], printer["filters"])) + printers.installPrinter(printer["path"], printer["name"]) + + logging.info("==> Successfully parsed a printer policy! ==") diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py new file mode 100644 index 0000000..52430c3 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/hooks.py @@ -0,0 +1,219 @@ +# +# This is used to run hooks +# + +from enum import Enum +import os, subprocess +from linuxmusterLinuxclient7 import logging, constants, user, config, computer, environment, setup, shares + +class Type(Enum): + """ + Enum containing all hook types + """ + + Boot = 0 + """The onBoot hook + """ + Shutdown = 1 + """The on Shutdown hook + """ + LoginAsRoot = 2 + """The onLoginAsRoot hook + """ + Login = 3 + """The onLogin hook + """ + SessionStarted = 4 + """The onSession started hook + """ + LogoutAsRoot = 5 + LoginLogoutAsRoot = 6 + +remoteScriptNames = { + Type.Boot: "sysstart.sh", + Type.Login: "logon.sh", + Type.SessionStarted: "sessionstart.sh", + Type.Shutdown: "sysstop.sh" +} + +_remoteScriptInUserContext = { + Type.Boot: False, + Type.Login: True, + Type.SessionStarted: True, + Type.Shutdown: False +} + +def runLocalHook(hookType): + """ + Run all scripts in a local hookdir + + :param hookType: The type of hook to run + :type hookType: hooks.Type + """ + logging.info("=== Running local hook on{0} ===".format(hookType.name)) + hookDir = _getLocalHookDir(hookType) + if os.path.exists(hookDir): + _prepareEnvironment() + for fileName in sorted(os.listdir(hookDir)): + filePath = hookDir + "/" + fileName + _runHookScript(filePath) + logging.info("===> Finished running local hook on{0} ===".format(hookType.name)) + + +def runRemoteHook(hookType): + """ + Run hookscript from sysvol + + :param hookType: The type of hook to run + :type hookType: hooks.Type + """ + logging.info("=== Running remote hook on{0} ===".format(hookType.name)) + rc, hookScripts = _getRemoteHookScripts(hookType) + + if rc: + _prepareEnvironment() + _runHookScript(hookScripts[0]) + _runHookScript(hookScripts[1]) + + logging.info("===> Finished running remote hook on{0} ===".format(hookType.name)) + +def runHook(hookType): + """ + Executes hooks.runLocalHook() and hooks.runRemoteHook() + + :param hookType: The type of hook to run + :type hookType: hooks.Type + """ + runLocalHook(hookType) + runRemoteHook(hookType) + +def getLocalHookScript(hookType): + """Get the path of a local hookscript + + :param hookType: The type of hook script to get the path for + :type hookType: hooks.Type + :return: The path + :rtype: str + """ + return "{0}/on{1}".format(constants.scriptDir,hookType.name) + +def shouldHooksBeExecuted(overrideUsername=None): + """Check if hooks should be executed + + :param overrideUsername: Override the username to check, defaults to None + :type overrideUsername: str, optional + :return: True if hooks should be executed, fale otherwise + :rtype: bool + """ + # check if linuxmuster-linuxclient7 is setup + if not setup.isSetup(): + logging.info("==== Linuxmuster-linuxclient7 is not setup, exiting ====") + return False + + # check if the computer is joined + if not computer.isInAD(): + logging.info("==== This Client is not joined to any domain, exiting ====") + return False + + # Check if the user is an AD user + if overrideUsername == None: + overrideUsername = user.username() + + if not user.isUserInAD(overrideUsername): + logging.info("==== {0} is not an AD user, exiting ====".format(user.username())) + return False + + return True + +# -------------------- +# - Helper functions - +# -------------------- + +def _prepareEnvironment(): + dictsAndPrefixes = {} + + rc, networkConfig = config.network() + if rc: + dictsAndPrefixes["Network"] = networkConfig + + rc, userConfig = user.readAttributes() + if rc: + dictsAndPrefixes["User"] = userConfig + + rc, computerConfig = computer.readAttributes() + if rc: + dictsAndPrefixes["Computer"] = computerConfig + + environment = _dictsToEnv(dictsAndPrefixes) + _writeEnvironment(environment) + +def _getLocalHookDir(hookType): + return "{0}/on{1}.d".format(constants.etcBaseDir,hookType.name) + +def _getRemoteHookScripts(hookType): + if not hookType in remoteScriptNames: + return False, None + + rc, networkConfig = config.network() + + if not rc: + logging.error("Could not execute server hooks because the network config could not be read") + return False, None + + if _remoteScriptInUserContext[hookType]: + rc, attributes = user.readAttributes() + if not rc: + logging.error("Could not execute server hooks because the user config could not be read") + return False, None + else: + rc, attributes = computer.readAttributes() + if not rc: + logging.error("Could not execute server hooks because the computer config could not be read") + return False, None + + try: + domain = networkConfig["domain"] + school = attributes["sophomorixSchoolname"] + scriptName = remoteScriptNames[hookType] + except: + logging.error("Could not execute server hooks because the computer/user config is missing attributes") + return False, None + + rc, sysvolPath = shares.getLocalSysvolPath() + if not rc: + logging.error("Could not execute server hook {} because the sysvol could not be mounted!\n") + return False, None + + hookScriptPathTemplate = "{0}/{1}/scripts/{2}/{3}/linux/{4}".format(sysvolPath, domain, school, "{}", scriptName) + + return True, [hookScriptPathTemplate.format("lmn"), hookScriptPathTemplate.format("custom")] + +# parameter must be a dict of {"prefix": dict} +def _dictsToEnv(dictsAndPrefixes): + environmentDict = {} + for prefix in dictsAndPrefixes: + for key in dictsAndPrefixes[prefix]: + if type(dictsAndPrefixes[prefix][key]) is list: + environmentDict[prefix + "_" + key] = "\n".join(dictsAndPrefixes[prefix][key]) + else: + environmentDict[prefix + "_" + key] = dictsAndPrefixes[prefix][key] + + return environmentDict + +def _runHookScript(filePath): + if not os.path.isfile(filePath): + logging.warning("* File {0} should be executed as hook but does not exist!".format(filePath)) + return + if not os.access(filePath, os.X_OK): + logging.warning("* File {0} is in hook dir but not executable!".format(filePath)) + return + + logging.info("== Executing script {0} ==".format(filePath)) + + result = subprocess.call([filePath]) + + logging.info("==> Script {0} finished with exit code {1} ==".format(filePath, result)) + +def _writeEnvironment(environment): + for key in environment: + os.putenv(key, environment[key]) diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py new file mode 100644 index 0000000..e0924b7 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/imageHelper.py @@ -0,0 +1,220 @@ +import os, subprocess, shutil +from linuxmusterLinuxclient7 import logging, setup, realm, user, constants, printers, fileHelper + +def prepareForImage(unattended=False): + """Prepare the computer for creating an image + + :param unattended: If set to True, all questions will be answered with yes, defaults to False + :type unattended: bool, optional + :return: True on success, False otherwise + :rtype: bool + """ + logging.info("#### Image preparation ####") + + try: + if not _testDomainJoin(unattended): + return False + + if not _upgradeSystem(unattended): + return False + + if not _clearCaches(unattended): + return False + + if not _clearUserHomes(unattended): + return False + + if not _clearUserCache(unattended): + return False + + if not _clearPrinters(unattended): + return False + + if not _clearLogs(unattended): + return False + + if not _emptyTrash(unattended): + return False + + except KeyboardInterrupt: + print() + logging.info("Cancelled.") + return False + + print() + logging.info("#### Image preparation done ####") + logging.info("#### You may create an Image now :) ####") + print() + return True + +# -------------------- +# - Helper functions - +# -------------------- + +def _askStep(step, printPlaceholder=True): + if printPlaceholder: + print() + response = input("Do you want to {}? (y/n): ".format(step)) + result = response in ["y", "Y", "j", "J"] + if result: + print() + return result + +def _testDomainJoin(unattended=False): + if not unattended and not _askStep("test if the domain join works"): + return True + + return setup.status() + +def _upgradeSystem(unattended=False): + if not unattended and not _askStep("update this computer now"): + return True + + # Perform an update + logging.info("Updating this computer now...") + + if subprocess.call(["apt", "update"]) != 0: + logging.error("apt update failed!") + return False + + if subprocess.call(["apt", "dist-upgrade", "-y"]) != 0: + logging.error("apt dist-upgrade failed!") + return False + + if subprocess.call(["apt", "autoremove", "-y"]) != 0: + logging.error("apt autoremove failed!") + return False + + if subprocess.call(["apt", "clean", "-y"]) != 0: + logging.error("apt clean failed!") + return False + + return True + +def _clearCaches(unattended=False): + if not unattended and not _askStep("clear journalctl and apt caches now"): + return True + + logging.info("Cleaning caches..") + logging.info("* apt") + fileHelper.deleteAllInDirectory("/var/lib/apt/lists/") + logging.info("* journalctl") + subprocess.call(["journalctl", "--flush", "--rotate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.call(["journalctl", "--vacuum-time=1s"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + logging.info("Done.") + return True + +def _checkLoggedInUsers(): + result = subprocess.run("who -s | awk '{ print $1 }'", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if result.returncode != 0: + logging.error("Failed to get logged in users!") + return False, None + + loggedInUsers = list(filter(None, result.stdout.split("\n"))) + + for loggedInUser in loggedInUsers: + if user.isUserInAD(loggedInUser): + logging.error("User {} is still logged in, please log out first! Aborting!".format(loggedInUser)) + return False + + return True + +def _clearUserCache(unattended=False): + if not unattended and not _askStep("clear all cached users now"): + return True + + if not _checkLoggedInUsers(): + return False + + realm.clearUserCache() + + logging.info("Done.") + + return realm.clearUserCache() + +def _unmountAllCifsMounts(): + logging.info("Unmounting all CIFS mounts!") + if subprocess.call(["umount", "-a", "-t", "cifs", "-l"]) != 0: + logging.info("Failed!") + return False + + # double check (just to be sure) + result = subprocess.run("mount", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if result.returncode != 0: + logging.error("Failed to get mounts!") + return False + + if ("cifs" in result.stdout) or ("CIFS" in result.stdout): + logging.error("There are still shares mounted!") + logging.info("Use \"mount | grep cifs\" to view them.") + return False + + return True + +def _clearUserHomes(unattended=False): + print("\nCAUTION! This will delete all userhomes of AD users!") + if not unattended and not _askStep("clear all user homes now", False): + return True + + if not _checkLoggedInUsers(): + return False + + if not _unmountAllCifsMounts(): + logging.info("Aborting deletion of user homes to prevent deleting data on the server.") + return False + + userHomes = os.listdir("/home") + + logging.info("Deleting all user homes now!") + for userHome in userHomes: + if not user.isUserInAD(userHome): + logging.info("* {} [SKIPPED]".format(userHome)) + continue + + logging.info("* {}".format(userHome)) + try: + shutil.rmtree("/home/{}".format(userHome)) + except Exception as e: + logging.error("* FAILED!") + logging.exception(e) + + try: + shutil.rmtree(constants.hiddenShareMountBasepath.format(userHome)) + except: + pass + + logging.info("Done.") + return True + +def _clearPrinters(unattended=False): + print("\nCAUTION! This will delete all printers of {}!".format(constants.templateUser)) + print("This makes sure that local printers do not conflict with remote printers defined by GPOs.") + if not unattended and not _askStep("remove all local printers of {}".format(constants.templateUser), False): + return True + + if not printers.uninstallAllPrintersOfUser(constants.templateUser): + return False + + return True + +def _clearLogs(unattended=False): + if not unattended and not _askStep("clear the syslog"): + return True + + if not fileHelper.deleteFile("/var/log/syslog"): + return False + + subprocess.call(["sudo", "service", "rsyslog", "restart"]) + + return True + +def _emptyTrash(unattended=False): + if not unattended and not _askStep("clear the Trash of linuxadmin"): + return True + + if not fileHelper.deleteAllInDirectory("/home/{}/.local/share/Trash".format(constants.templateUser)): + return False + + return True \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py new file mode 100644 index 0000000..ce8556a --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/keytab.py @@ -0,0 +1,52 @@ +from krb5KeytabUtil import Krb5KeytabUtil +from linuxmusterLinuxclient7 import computer, config, logging + +def patchKeytab(): + """ + Patches the `/etc/krb5.keytab` file. It inserts the correct hostname of the current computer. + + :return: True on success, False otherwise + :rtype: bool + """ + krb5KeytabFilePath = "/etc/krb5.keytab" + logging.info("Patching {}".format(krb5KeytabFilePath)) + krb5KeytabUtil = Krb5KeytabUtil(krb5KeytabFilePath) + + try: + krb5KeytabUtil.read() + except: + logging.error("Error reading {}".format(krb5KeytabFilePath)) + return False + + for entry in krb5KeytabUtil.keytab.entries: + oldData = entry.principal.components[-1].data + if len(entry.principal.components) == 1: + newData = computer.hostname().upper() + "$" + entry.principal.components[0].data = newData + + elif len(entry.principal.components) == 2 and (entry.principal.components[0].data == "host" or entry.principal.components[0].data == "RestrictedKrbHost"): + rc, networkConfig = config.network() + if not rc: + continue + + newData = "" + domain = networkConfig["domain"] + if domain in entry.principal.components[1].data: + newData = computer.hostname().lower() + "." + domain + else: + newData = computer.hostname().upper() + + entry.principal.components[1].data = newData + + logging.debug("{} was changed to {}".format(oldData, entry.principal.components[-1].data)) + + logging.info("Trying to overwrite {}".format(krb5KeytabFilePath)) + try: + result = krb5KeytabUtil.write() + except: + result = False + + if not result: + logging.error("Error overwriting {}".format(krb5KeytabFilePath)) + + return result diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py new file mode 100644 index 0000000..c9856f4 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/ldapHelper.py @@ -0,0 +1,148 @@ +import ldap, ldap.sasl, sys, getpass, subprocess +from linuxmusterLinuxclient7 import logging, constants, config, user, computer + +_currentLdapConnection = None + +def serverUrl(): + """ + Returns the server URL + + :return: The server URL + :rtype: str + """ + rc, networkConfig = config.network() + + if not rc: + return False, None + + serverHostname = networkConfig["serverHostname"] + return 'ldap://{0}'.format(serverHostname) + +def baseDn(): + """ + Returns the base DN + + :return: The baseDN + :rtype: str + """ + rc, networkConfig = config.network() + + if not rc: + return None + + domain = networkConfig["domain"] + return "dc=" + domain.replace(".", ",dc=") + +def conn(): + """ + Returns the ldap connection object + + :return: The ldap connection object + :rtype: ldap.ldapobject.LDAPObject + """ + global _currentLdapConnection + + if _connect(): + return _currentLdapConnection + + return None + +def searchOne(filter): + """Searches the LDAP with a filter and returns the first found object + + :param filter: A valid ldap filter + :type filter: str + :return: Tuple (success, ldap object as dict) + :rtype: tuple + """ + if conn() == None: + logging.error("Cannot talk to LDAP") + return False, None + + try: + rawResult = conn().search_s( + baseDn(), + ldap.SCOPE_SUBTREE, + filter + ) + except Exception as e: + logging.error("Error executing LDAP search!") + logging.exception(e) + return False, None + + try: + result = {} + + if len(rawResult) <= 0 or rawResult[0][0] == None: + logging.debug(f"Search \"{filter}\" did not return any objects") + return False, None + + for k in rawResult[0][1]: + if rawResult[0][1][k] != None: + rawAttribute = rawResult[0][1][k] + try: + if len(rawAttribute) == 1: + result[k] = str(rawAttribute[0].decode()) + elif len(rawAttribute) > 0: + result[k] = [] + for rawItem in rawAttribute: + result[k].append(str(rawItem.decode())) + except UnicodeDecodeError: + continue + + return True, result + + except Exception as e: + logging.error("Error while reading ldap search results!") + logging.exception(e) + return False, None + +def isObjectInGroup(objectDn, groupName): + """ + Check if a given object is in a given group + + :param objectDn: The DN of the object + :type objectDn: str + :param groupName: The name of the group + :type groupName: str + :return: True if it is a member, False otherwise + :rtype: bool + """ + logging.debug("= Testing if object {0} is a member of group {1} =".format(objectDn, groupName)) + rc, groupAdObject = searchOne("(&(member:1.2.840.113556.1.4.1941:={0})(sAMAccountName={1}))".format(objectDn, groupName)) + logging.debug("=> Result: {} =".format(rc)) + return rc + +# -------------------- +# - Helper functions - +# -------------------- + +def _connect(): + global _currentLdapConnection + + if not user.isInAD() and not (user.isRoot() or not computer.isInAD()): + logging.warning("Cannot perform LDAP search: User is not in AD!") + _currentLdapConnection = None + return False + + if not _currentLdapConnection == None: + return True + + try: + sasl_auth = ldap.sasl.sasl({} ,'GSSAPI') + _currentLdapConnection = ldap.initialize(serverUrl(), trace_level=0) + # TODO: + # conn.set_option(ldap.OPT_X_TLS_CACERTFILE, '/path/to/ca.pem') + # conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + # conn.start_tls_s() + _currentLdapConnection.set_option(ldap.OPT_REFERRALS,0) + _currentLdapConnection.protocol_version = ldap.VERSION3 + + _currentLdapConnection.sasl_interactive_bind_s("", sasl_auth) + except Exception as e: + _currentLdapConnection = None + logging.error("Cloud not bind to ldap!") + logging.exception(e) + return False + + return True \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/localUserHelper.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/localUserHelper.py new file mode 100644 index 0000000..f1adcc8 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/localUserHelper.py @@ -0,0 +1,19 @@ +import subprocess +from linuxmusterLinuxclient7 import logging + +def getGroupsOfLocalUser(username): + """ + Get all groups of a local user + + :param username: The username of the user + :type username: str + :return: Tuple (success, list of groups) + :rtype: tuple + """ + try: + groups = subprocess.check_output(["id", "-Gnz", username]) + stringList=[x.decode('utf-8') for x in groups.split(b"\x00")] + return True, stringList + except Exception as e: + logging.warning("Exception when querying groups of user {}, it probaply does not exist".format(username)) + return False, None \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py new file mode 100644 index 0000000..f73cb68 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/logging.py @@ -0,0 +1,130 @@ +import logging, os, traceback, re, sys, subprocess +from enum import Enum +from linuxmusterLinuxclient7 import user, config + +class Level(Enum): + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + FATAL = 4 + +def debug(message): + """ + Do a debug log. + + :param message: The message to log + :type message: str + """ + _log(Level.DEBUG, message) + +def info(message): + """ + Do an info log. + + :param message: The message to log + :type message: str + """ + _log(Level.INFO, message) + +def warning(message): + """ + Do a warning log. + + :param message: The message to log + :type message: str + """ + _log(Level.WARNING, message) + +def error(message): + """ + Do an error log. + + :param message: The message to log + :type message: str + """ + _log(Level.ERROR, message) + +def fatal(message): + """ + Do a fatal log. If used in onLogin hook, this will create a dialog containing the message. + + :param message: The message to log + :type message: str + """ + _log(Level.FATAL, message) + +def exception(exception): + """ + Log an exception + + :param exception: The exception to log + :type exception: Exception + """ + error("=== An exception occurred ===") + error(str(exception)) + # Only use for debugging! This will cause ugly error dialogs in X11 + #traceback.print_tb(exception.__traceback__) + error("=== end exception ===") + +def printLogs(compact=False,anonymize=False): + """ + Print logs of linuxmuster-linuxclient7 from `/var/log/syslog`. + + :param compact: If set to True, some stuff like time and date will be removed. Defaults to False + :type compact: bool, optional + :param anonymize: If set to True, domain/realm/serverHostname will be replaced by linuxmuster.lan. Defaults to False + :type anonymize: bool, optional + """ + print("===========================================") + print("=== Linuxmuster-linuxclient7 logs begin ===") + + (rc, networkConfig) = config.network() + if rc: + domain = networkConfig["domain"] + serverHostname = networkConfig["serverHostname"] + realm= networkConfig["realm"] + + with open("/var/log/syslog") as logfile: + startPattern = re.compile("^.*linuxmuster-linuxclient7[^>]+======$") + endPattern = re.compile("^.*linuxmuster-linuxclient7.*======>.*$") + + currentlyInsideOfLinuxmusterLinuxclient7Log = False + + for line in logfile: + line = line.replace("\n", "") + if startPattern.fullmatch(line): + currentlyInsideOfLinuxmusterLinuxclient7Log = True + print("\n") + + if currentlyInsideOfLinuxmusterLinuxclient7Log: + if compact: + # "^([^ ]+[ ]+){4}" matches "Apr 6 14:39:23 somehostname" + line = re.sub("^([^ ]+[ ]+){4}", "", line) + if anonymize and rc: + line = re.sub(serverHostname, "server.linuxmuster.lan", line) + line = re.sub(domain, "linuxmuster.lan", line) + line = re.sub(realm, "LINUXMUSTER.LAN", line) + + print(line) + + if endPattern.fullmatch(line): + currentlyInsideOfLinuxmusterLinuxclient7Log = False + print("\n") + + print("=== Linuxmuster-linuxclient7 logs end ===") + print("=========================================") + +# -------------------- +# - Helper functions - +# -------------------- + +def _log(level, message): + #if level == Level.DEBUG: + # return + if level == Level.FATAL: + sys.stderr.write(message) + + print("[{0}] {1}".format(level.name, message)) + message = message.replace("'", "") + subprocess.call(["logger", "-t", "linuxmuster-linuxclient7", f"[{level.name}] {message}"]) diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py new file mode 100644 index 0000000..d4e42a1 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/printers.py @@ -0,0 +1,129 @@ +import os, subprocess, re +from linuxmusterLinuxclient7 import logging, user + +def installPrinter(networkPath, name=None, username=None): + """ + Installs a networked printer for a user + + :param networkPath: The network path of the printer + :type networkPath: str + :param name: The name for the printer, defaults to None + :type name: str, optional + :param username: The username of the user whom the is installed printer for. Defaults to the executing user + :type username: str, optional + :return: True on success, False otherwise + :rtype: bool + """ + if username == None: + username = user.username() + + if user.isRoot(): + return _installPrinter(username, networkPath, name) + else: + # This will call installPrinter() again with root privileges + return _installPrinterWithoutRoot(networkPath, name) + + pass + +def uninstallAllPrintersOfUser(username): + """ + Uninstalls all printers of a given user + + :param username: The username of the user + :type username: str + :return: True on success, False otherwise + :rtype: bool + """ + logging.info("Uninstalling all printers of {}".format(username)) + rc, installedPrinters = _getInstalledPrintersOfUser(username) + + if not rc: + logging.error("Error getting printers!") + return False + + for installedPrinter in installedPrinters: + if not _uninstallPrinter(installedPrinter): + return False + + return True + +def translateSambaToIpp(networkPath): + """ + Translates a samba url, like `\\server\PRINTER-01`, to an ipp url like `ipp://server/printers/PRINTER-01`. + + :param networkPath: The samba url + :type networkPath: str + :return: An ipp url + :rtype: str + """ + networkPath = networkPath.replace("\\", "/") + # path has to be translated: \\server\EW-FARBLASER -> ipp://server/printers/EW-farblaser + pattern = re.compile("\\/\\/([^/]+)\\/(.*)") + + result = pattern.findall(networkPath) + if len(result) != 1 or len(result[0]) != 2: + logging.error("Cannot convert printer network path from samba to ipp, as it is invalid: {}".format(networkPath)) + return False, None + + ippNetworkPath = "ipp://{0}/printers/{1}".format(result[0][0], result[0][1]) + return True, ippNetworkPath + +# -------------------- +# - Helper functions - +# -------------------- + +def _installPrinter(username, networkPath, name): + logging.info("Install Printer {0} on {1}".format(name, networkPath)) + installCommand = ["timeout", "10", "lpadmin", "-p", name, "-E", "-v", networkPath, "-m", "everywhere", "-u", f"allow:{username}"] + logging.debug("* running '{}'".format(" ".join(installCommand))) + p = subprocess.call(installCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p == 0: + logging.debug("* Success Install Printer!") + return True + elif p == 124: + logging.fatal(f"* Timeout error while installing printer {name} on {networkPath}") + else: + logging.fatal(f"* Error installing printer {name} on {networkPath}!") + return False + +def _installPrinterWithoutRoot(networkPath, name): + return subprocess.call(["sudo", "/usr/share/linuxmuster-linuxclient7/scripts/sudoTools", "install-printer", "--path", networkPath, "--name", name]) == 0 + +def _getInstalledPrintersOfUser(username): + logging.info(f"Getting installed printers of {username}") + command = f"lpstat -U {username} -p" + #logging.debug("running '{}'".format(command)) + + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if not result.returncode == 0: + logging.info("No Printers installed.") + return True, [] + + rawInstalledPrinters = list(filter(None, result.stdout.split("\n"))) + installedPrinters = [] + + for rawInstalledPrinter in rawInstalledPrinters: + rawInstalledPrinterList = list(filter(None, rawInstalledPrinter.split(" "))) + + if len(rawInstalledPrinterList) < 2: + continue + + installedPrinter = rawInstalledPrinterList[1] + installedPrinters.append(installedPrinter) + + return True, installedPrinters + +def _uninstallPrinter(name): + logging.info("Uninstall Printer {}".format(name)) + uninstallCommand = ["timeout", "10", "lpadmin", "-x", name] + logging.debug("* running '{}'".format(" ".join(uninstallCommand))) + p = subprocess.call(uninstallCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p == 0: + logging.debug("* Success Uninstall Printer!") + return True + elif p == 124: + logging.fatal(f"* Timeout error while installing printer {name}") + else: + logging.fatal(f"* Error Uninstalling Printer {name}!") + return False diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py new file mode 100644 index 0000000..3847529 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/realm.py @@ -0,0 +1,195 @@ +import os, sys, subprocess, configparser +from linuxmusterLinuxclient7 import logging, computer + +def join(domain, user): + """ + Joins the computer to an AD + + :param domain: The domain to join + :type domain: str + :param user: The admin user used for joining + :type user: str + :return: True on success, False otherwise + :rtype: bool + """ + # join the domain using the kerberos ticket + joinCommand = ["realm", "join", "-v", domain, "-U", user] + if subprocess.call(joinCommand) != 0: + print() + logging.error('Failed! Did you enter the correct password?') + return False + + logging.info("It looks like the domain was joined successfully.") + return True + +def leave(domain): + """ + Leave a domain + + :param domain: The domain to leave + :type domain: str + :return: True on success, False otherwise + :rtype: bool + """ + leaveCommand = ["realm", "leave", domain] + return subprocess.call(leaveCommand) == 0 + +def leaveAll(): + """ + Leaves all joined domains + + :return: True on success, False otherwise + :rtype: bool + """ + logging.info("Cleaning / leaving all domain joins") + + rc, joinedDomains = getJoinedDomains() + if not rc: + return False + + for joinedDomain in joinedDomains: + logging.info("* {}".format(joinedDomain)) + if not leave(joinedDomain): + logging.error("-> Failed! Aborting!") + return False + + logging.info("-> Done!") + return True + +def isJoined(): + """ + Checks if the computer is joined to a domain + + :return: True if it is joined to one or more domains, False otherwise + :rtype: bool + """ + rc, joinedDomains = getJoinedDomains() + if not rc: + return False + else: + return len(joinedDomains) > 0 + +def pullKerberosTicketForComputerAccount(): + """ + Pulls a kerberos ticket using the computer account from `/etc/krb5.keytab` + + :return: True on success, False otherwise + :rtype: bool + """ + return subprocess.call(["kinit", "-k", computer.krbHostName()]) == 0 + +def verifyDomainJoin(): + """ + Checks if the domain join actually works. + + :return: True if it does, False otherwise + :rtype: bool + """ + logging.info("Testing if the domain join actually works") + if not isJoined(): + logging.error("No domain is joined!") + return False + + logging.info("* Checking if the group \"domain users\" exists") + if subprocess.call(["getent", "group", "domain users"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) != 0: + logging.error("The \"domain users\" group does not exists! Users wont be able to log in!") + logging.error("This is sometimes related to /etc/nsswitch.conf.") + return False + + # Try to get a kerberos ticket for the computer account + logging.info("* Trying to get a kerberos ticket for the Computer Account") + if not pullKerberosTicketForComputerAccount(): + logging.error("Could not get a kerberos ticket for the Computer Account!") + logging.error("Logins of non-cached users WILL NOT WORK!") + logging.error("Please try to re-join the Domain.") + return False + + + logging.info("The domain join is working!") + return True + +def getJoinedDomains(): + """ + Returns all joined domains + + :return: Tuple (success, list of joined domians) + :rtype: tuple + """ + result = subprocess.run("realm list --name-only", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if result.returncode != 0: + logging.error("Failed to read domain joins!") + return False, None + + return True, list(filter(None, result.stdout.split("\n"))) + +def discoverDomains(): + """ + Searches for avialable domains on the current network + + :return: Tuple (success, list of available domains) + :rtype: tuple + """ + result = subprocess.run("realm discover --name-only", stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if result.returncode != 0: + logging.error("Failed to discover available domains!") + return False, None + + return True, list(filter(None, result.stdout.split("\n"))) + +def getDomainConfig(domain): + """ + Looks up all relevant properties of a domain: + - domain controller IP + - domain name + + :param domain: The domain to check + :type domain: str + :return: Tuple (success, dict with domain config) + :rtype: tuple + """ + result = subprocess.run("adcli info '{}'".format(domain), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if result.returncode != 0: + logging.error("Failed to get details of domain {}!".format(domain)) + return False, None + + rawConfig = _readConfigFromString(result.stdout) + try: + rawDomainConfig = rawConfig["domain"] + except KeyError: + logging.error("Error when reading domain details") + return False, None + + domainConfig = {} + + try: + domainConfig["domain-controller"] = rawDomainConfig["domain-controller"] + domainConfig["domain-name"] = rawDomainConfig["domain-name"] + except KeyError: + logging.error("Error when reading domain details (2)") + return False, None + + return True, domainConfig + +def clearUserCache(): + """ + Clears the local user cache + + :return: True on success, False otherwise + :rtype: bool + """ + # clean sssd cache + logging.info("Cleaning sssd cache.") + subprocess.call(["sssctl", "cache-remove", "--stop", "--start", "--override"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return True + +# -------------------- +# - Helper functions - +# -------------------- + +def _readConfigFromString(string): + configParser = configparser.ConfigParser() + configParser.read_string(string) + return configParser \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py new file mode 100644 index 0000000..85994fe --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/setup.py @@ -0,0 +1,364 @@ +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 \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py new file mode 100644 index 0000000..64a879f --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/shares.py @@ -0,0 +1,256 @@ +import os, pwd, sys, shutil, re, subprocess, shutil +from linuxmusterLinuxclient7 import logging, constants, user, config, computer +from pathlib import Path + +def mountShare(networkPath, shareName = None, hiddenShare = False, username = None): + """ + Mount a given path of a samba share + + :param networkPath: Network path of the share + :type networkPath: str + :param shareName: The name of the share (name of the folder the share is being mounted to) + :type shareName: str + :param hiddenShare: If the share sould be visible in Nautilus + :type hiddenShare: bool + :param username: The user in whoms context the share should be mounted + :type username: str + :return: Tuple: (success, mountpoint) + :rtype: tuple + """ + networkPath = networkPath.replace("\\", "/") + username = _getDefaultUsername(username) + shareName = _getDefaultShareName(networkPath, shareName) + + if user.isRoot(): + return _mountShare(username, networkPath, shareName, hiddenShare, True) + else: + mountpoint = _getShareMountpoint(networkPath, username, hiddenShare, shareName) + # This will call _mountShare() directly with root privileges + return _mountShareWithoutRoot(networkPath, shareName, hiddenShare), mountpoint + +def getMountpointOfRemotePath(remoteFilePath, hiddenShare = False, username = None, autoMount = True): + """ + Get the local path of a remote samba share path. + This function automatically checks if the shares is already mounted. + It optionally automatically mounts the top path of the remote share: + If the remote path is `//server/sysvol/linuxmuster.lan/Policies` it mounts `//server/sysvol` + + :param remoteFilePath: Remote path + :type remoteFilePath: str + :param hiddenShare: If the share sould be visible in Nautilus + :type hiddenShare: bool + :param username: The user in whoms context the share should be mounted + :type username: str + :parama autoMount: If the share should be mouted automatically if it is not already mounted + :type autoMount: bool + :return: Tuple: (success, mountpoint) + :rtype: tuple + """ + remoteFilePath = remoteFilePath.replace("\\", "/") + username = _getDefaultUsername(username) + + # get basepath fo remote file path + # this turns //server/sysvol/linuxmuster.lan/Policies into //server/sysvol + pattern = re.compile("(^\\/\\/[^\\/]+\\/[^\\/]+)") + match = pattern.search(remoteFilePath) + + if match is None: + logging.error("Cannot get local file path of {} beacuse it is not a valid path!".format(remoteFilePath)) + return False, None + + shareBasepath = match.group(0) + + if autoMount: + rc, mointpoint = mountShare(shareBasepath, hiddenShare=hiddenShare, username=username) + if not rc: + return False, None + + # calculate local path + shareMountpoint = _getShareMountpoint(shareBasepath, username, hiddenShare, shareName=None) + localFilePath = remoteFilePath.replace(shareBasepath, shareMountpoint) + + return True, localFilePath + +def unmountAllSharesOfUser(username): + """ + Unmount all shares of a given user and safely delete the mountpoints and the parent directory. + + :param username: The username of the user + :type username: str + :return: True or False + :rtype: bool + """ + logging.info("=== Trying to unmount all shares of user {0} ===".format(username)) + for basedir in [constants.shareMountBasepath, constants.hiddenShareMountBasepath]: + shareMountBasedir = basedir.format(username) + + try: + mountedShares = os.listdir(shareMountBasedir) + except FileNotFoundError: + logging.info("Mount basedir {} does not exist -> nothing to unmount".format(shareMountBasedir)) + continue + + for share in mountedShares: + _unmountShare("{0}/{1}".format(shareMountBasedir, share)) + + if len(os.listdir(shareMountBasedir)) > 0: + logging.warning("* Mount basedir {} is not empty so not removed!".format(shareMountBasedir)) + return False + else: + # Delete the directory + logging.info("Deleting {0}...".format(shareMountBasedir)) + try: + os.rmdir(shareMountBasedir) + except Exception as e: + logging.error("FAILED!") + logging.exception(e) + return False + + logging.info("===> Finished unmounting all shares of user {0} ===".format(username)) + return True + +def getLocalSysvolPath(): + """ + Get the local mountpoint of the sysvol + + :return: Full path of the mountpoint + :rtype: str + """ + rc, networkConfig = config.network() + if not rc: + return False, None + + networkPath = f"//{networkConfig['serverHostname']}/sysvol" + return getMountpointOfRemotePath(networkPath, True) + +# -------------------- +# - Helper functions - +# -------------------- + +# useCruidOfExecutingUser: +# defines if the ticket cache of the user executing the mount command should be used. +# If set to False, the cache of the user with the given username will be used. +# This parameter influences the `cruid` mount option. +def _mountShare(username, networkPath, shareName, hiddenShare, useCruidOfExecutingUser=False): + + mountpoint = _getShareMountpoint(networkPath, username, hiddenShare, shareName) + + mountCommandOptions = f"file_mode=0700,dir_mode=0700,sec=krb5,nodev,nosuid,mfsymlinks,nobrl,vers=3.0,user={username}" + rc, networkConfig = config.network() + domain = None + + if rc: + domain = networkConfig["domain"] + mountCommandOptions += f",domain={domain.upper()}" + + try: + pwdInfo = pwd.getpwnam(username) + uid = pwdInfo.pw_uid + gid = pwdInfo.pw_gid + mountCommandOptions += f",gid={gid},uid={uid}" + + if not useCruidOfExecutingUser: + mountCommandOptions += f",cruid={uid}" + + except KeyError: + uid = -1 + gid = -1 + logging.warning("Uid could not be found! Continuing anyway!") + + mountCommand = [shutil.which("mount.cifs"), "-o", mountCommandOptions, networkPath, mountpoint] + + logging.debug(f"Trying to mount '{networkPath}' to '{mountpoint}'") + logging.debug("* Creating directory...") + + try: + Path(mountpoint).mkdir(parents=True, exist_ok=False) + except FileExistsError: + # Test if a share is already mounted there + if _directoryIsMountpoint(mountpoint): + logging.debug("* The mountpoint is already mounted.") + return True, mountpoint + else: + logging.warning("* The target directory already exists, proceeding anyway!") + + logging.debug("* Executing '{}' ".format(" ".join(mountCommand))) + logging.debug("* Trying to mount...") + if not subprocess.call(mountCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0: + logging.fatal(f"* Error mounting share {networkPath} to {mountpoint}!\n") + return False, None + + logging.debug("* Success!") + + # hide the shares parent dir (/home/%user/media) in case it is not a hidden share + if not hiddenShare: + try: + hiddenFilePath = f"{mountpoint}/../../.hidden" + logging.debug(f"* hiding parent dir {hiddenFilePath}") + hiddenFile = open(hiddenFilePath, "w+") + hiddenFile.write(mountpoint.split("/")[-2]) + hiddenFile.close() + except: + logging.warning(f"Could not hide parent dir of share {mountpoint}") + + return True, mountpoint + +def _unmountShare(mountpoint): + # check if mountpoint exists + if (not os.path.exists(mountpoint)) or (not os.path.isdir(mountpoint)): + logging.warning(f"* Could not unmount {mountpoint}, it does not exist.") + + # Try to unmount share + logging.info("* Trying to unmount {0}...".format(mountpoint)) + if not subprocess.call(["umount", mountpoint]) == 0: + logging.warning("* Failed!") + if _directoryIsMountpoint(mountpoint): + logging.warning("* It is still mounted! Exiting!") + # Do not delete in this case! We might delete userdata! + return + logging.info("* It is not mounted! Continuing!") + + # check if the mountpoint is empty + if len(os.listdir(mountpoint)) > 0: + logging.warning("* mountpoint {} is not empty so not removed!".format(mountpoint)) + return + + # Delete the directory + logging.info("* Deleting {0}...".format(mountpoint)) + try: + os.rmdir(mountpoint) + except Exception as e: + logging.error("* FAILED!") + logging.exception(e) + +def _getDefaultUsername(username=None): + if username == None: + if user.isRoot(): + username = computer.hostname().upper() + "$" + else: + username = user.username() + return username + +def _getDefaultShareName(networkPath, shareName=None): + if shareName is None: + shareName = networkPath.split("/")[-1] + return shareName + +def _mountShareWithoutRoot(networkPath, name, hidden): + mountCommand = ["sudo", "/usr/share/linuxmuster-linuxclient7/scripts/sudoTools", "mount-share", "--path", networkPath, "--name", name] + + if hidden: + mountCommand.append("--hidden") + + return subprocess.call(mountCommand) == 0 + +def _getShareMountpoint(networkPath, username, hidden, shareName = None): + logging.debug(f"Calculating mountpoint of {networkPath}") + + shareName = _getDefaultShareName(networkPath, shareName) + + if hidden: + return "{0}/{1}".format(constants.hiddenShareMountBasepath.format(username), shareName) + else: + return "{0}/{1}".format(constants.shareMountBasepath.format(username), shareName) + +def _directoryIsMountpoint(dir): + return subprocess.call(["mountpoint", "-q", dir]) == 0 diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py new file mode 100644 index 0000000..b634fd2 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/templates.py @@ -0,0 +1,128 @@ +import os, codecs, sys, shutil, subprocess +from pathlib import Path +from linuxmusterLinuxclient7 import logging, constants, hooks, config + + +def applyAll(): + """ + Applies all templates from `/usr/share/linuxmuster-linuxclient7/templates` + + :return: True on success, False otherwise + :rtype: bool + """ + logging.info('Applying all configuration templates:') + + templateDir = constants.configFileTemplateDir + for templateFile in os.listdir(templateDir): + templatePath = templateDir + '/' + templateFile + logging.info('* ' + templateFile + ' ...') + if not _apply(templatePath): + logging.error("Aborting!") + return False + + # reload sctemctl + logging.info('Reloading systemctl ... ') + if not subprocess.call(["systemctl", "daemon-reload"]) == 0: + logging.error("Failed!") + return False + + return True + +# -------------------- +# - Helper functions - +# -------------------- + + +def _apply(templatePath): + try: + # read template file + rc, fileData = _readTextfile(templatePath) + + if not rc: + logging.error('Failed!') + return False + + fileData = _resolveVariables(fileData) + + # get target path + firstLine = fileData.split('\n')[0] + targetFilePath = firstLine.partition(' ')[2] + + # remove first line (the target file path) + fileData = fileData[fileData.find('\n'):] + + # never ever overwrite sssd.conf, this will lead to issues! + # sssd.conf is written by `realm join`! + if targetFilePath in constants.notTemplatableFiles: + logging.warning("Skipping forbidden file {}".format(targetFilePath)) + return True + + # create target directory + Path(Path(targetFilePath).parent.absolute()).mkdir(parents=True, exist_ok=True) + + # remove comment lines beginning with # from .xml files + if targetFilePath.endswith('.xml'): + fileData = _stripComment(fileData) + + # write config file + logging.debug("-> to {}".format(targetFilePath)) + with open(targetFilePath, 'w') as targetFile: + targetFile.write(fileData) + + return True + + except Exception as e: + logging.error('Failed!') + logging.exception(e) + return False + +def _resolveVariables(fileData): + # replace placeholders with values + rc, networkConfig = config.network() + + if not rc: + return False, None + + # network + fileData = fileData.replace('@@serverHostname@@', networkConfig["serverHostname"]) + fileData = fileData.replace('@@domain@@', networkConfig["domain"]) + fileData = fileData.replace('@@realm@@', networkConfig["realm"]) + + # constants + fileData = fileData.replace('@@userTemplateDir@@', constants.userTemplateDir) + fileData = fileData.replace('@@hiddenShareMountBasepath@@', constants.hiddenShareMountBasepath.format("%(USER)")) + + # hooks + fileData = fileData.replace('@@hookScriptBoot@@', hooks.getLocalHookScript(hooks.Type.Boot)) + fileData = fileData.replace('@@hookScriptShutdown@@', hooks.getLocalHookScript(hooks.Type.Shutdown)) + fileData = fileData.replace('@@hookScriptLoginLogoutAsRoot@@', hooks.getLocalHookScript(hooks.Type.LoginLogoutAsRoot)) + fileData = fileData.replace('@@hookScriptSessionStarted@@', hooks.getLocalHookScript(hooks.Type.SessionStarted)) + + return fileData + +# read textfile in variable +def _readTextfile(filePath): + if not os.path.isfile(filePath): + return False, None + try: + infile = codecs.open(filePath ,'r', encoding='utf-8', errors='ignore') + content = infile.read() + infile.close() + return True, content + except Exception as e: + logging.info('Cannot read ' + filePath + '!') + logging.exception(e) + return False, None + +# remove lines beginning with # +def _stripComment(fileData): + filedata_stripped = '' + for line in fileData.split('\n'): + if line[:1] == '#': + continue + else: + if filedata_stripped == '': + filedata_stripped = line + else: + filedata_stripped = filedata_stripped + '\n' + line + return filedata_stripped \ No newline at end of file diff --git a/roles/lmn_printer/files/linuxmusterLinuxclient7/user.py b/roles/lmn_printer/files/linuxmusterLinuxclient7/user.py new file mode 100644 index 0000000..aa838f0 --- /dev/null +++ b/roles/lmn_printer/files/linuxmusterLinuxclient7/user.py @@ -0,0 +1,158 @@ +import ldap, ldap.sasl, sys, getpass, subprocess, pwd, os, os.path +from pathlib import Path +from linuxmusterLinuxclient7 import logging, constants, config, user, ldapHelper, shares, fileHelper, computer, localUserHelper + +def readAttributes(): + """ + Reads all attributes of the current user from ldap + + :return: Tuple (success, dict of user attributes) + :rtype: tuple + """ + if not user.isInAD(): + return False, None + + return ldapHelper.searchOne(f"(sAMAccountName={user.username()})") + +def school(): + """ + Gets the school of the current user from the AD + + :return: The short name of the school + :rtype: str + """ + rc, userdata = readAttributes() + + if not rc: + return False, None + + return True, userdata["sophomorixSchoolname"] + +def username(): + """ + Returns the user of the current user + + :return: The username of the current user + :rtype: str + """ + return getpass.getuser().lower() + +def isUserInAD(user): + """ + Checks if a given user is an AD user. + + :param user: The username of the user to check + :type user: str + :return: True if the user is in the AD, False if it is a local user + :rtype: bool + """ + if not computer.isInAD(): + return False + + rc, groups = localUserHelper.getGroupsOfLocalUser(user) + if not rc: + return False + + return "domain users" in groups + +def isInAD(): + """Checks if the current user is an AD user. + + :return: True if the user is in the AD, False if it is a local user + :rtype: bool + """ + return isUserInAD(username()) + +def isRoot(): + """ + Checks if the current user is root + + :return: True if the current user is root, False otherwise + :rtype: bool + """ + return os.geteuid() == 0 + +def isInGroup(groupName): + """ + Checks if the current user is part of a given group + + :param groupName: The name of the group + :type groupName: str + :return: True if the user is part of the group, False otherwise + :rtype: bool + """ + rc, groups = localUserHelper.getGroupsOfLocalUser(username()) + if not rc: + return False + + return groupName in groups + +def cleanTemplateUserGtkBookmarks(): + """Remove gtk bookmarks of the template user from the current users `~/.config/gtk-3.0/bookmarks` file. + """ + logging.info("Cleaning {} gtk bookmarks".format(constants.templateUser)) + gtkBookmarksFile = "/home/{0}/.config/gtk-3.0/bookmarks".format(user.username()) + + if not os.path.isfile(gtkBookmarksFile): + logging.warning("Gtk bookmarks file not found, skipping!") + return + + fileHelper.removeLinesInFileContainingString(gtkBookmarksFile, constants.templateUser) + +def getHomeShareMountpoint(): + """ + Returns the mountpoint of the users serverhome. + + :return: The monutpoint of the users serverhome + :rtype: str + """ + rc, homeShareName = _getHomeShareName() + + if rc: + basePath = constants.shareMountBasepath.format(username()) + return True, f"{basePath}/{homeShareName}" + + return False, None + +def mountHomeShare(): + """ + Mounts the serverhome of the current user + + :return: True on success, False otherwise + :rtype: bool + """ + rc1, userAttributes = readAttributes() + rc2, shareName = _getHomeShareName(userAttributes) + if rc1 and rc2: + try: + homeShareServerPath = userAttributes["homeDirectory"] + res = shares.mountShare(homeShareServerPath, shareName=shareName, hiddenShare=False, username=username()) + return res + + except Exception as e: + logging.error("Could not mount home dir of user") + logging.exception(e) + + return False, None + +# -------------------- +# - Helper functions - +# -------------------- + +def _getHomeShareName(userAttributes=None): + if userAttributes is None: + rc, userAttributes = readAttributes() + else: + rc = True + + if rc: + try: + usernameString = username() + shareName = f"{usernameString} ({userAttributes['homeDrive']})" + return True, shareName + + except Exception as e: + logging.error("Could not mount home dir of user") + logging.exception(e) + + return False, None \ No newline at end of file diff --git a/roles/lmn_printer/files/lmn-printer.sh b/roles/lmn_printer/files/lmn-printer.sh new file mode 100644 index 0000000..e103068 --- /dev/null +++ b/roles/lmn_printer/files/lmn-printer.sh @@ -0,0 +1 @@ +[[ "${UID}" -gt 10000 ]] && /usr/local/bin/onLogin diff --git a/roles/lmn_printer/files/onLogin b/roles/lmn_printer/files/onLogin new file mode 100644 index 0000000..565b80f --- /dev/null +++ b/roles/lmn_printer/files/onLogin @@ -0,0 +1,33 @@ +#!/usr/bin/python3 + +# DO NOT MODIFY THIS SCRIPT! +# For custom scripts use the hookdir /etc/linuxmuster-linuxclient7/onLogin.d + +# This schript is called in user context when a user logs in +try: + import os, sys + #import traceback + from linuxmusterLinuxclient7 import logging, hooks, shares, user, constants, gpo, computer, environment + + logging.info("====== onLogin started ======") + + # mount sysvol + rc, sysvolPath = shares.getLocalSysvolPath() + if rc: + environment.export(f"SYSVOL={sysvolPath}") + + # process GPOs + gpo.processAllPolicies() + + logging.info("======> onLogin end ======") + +except Exception as e: + try: + #traceback.print_exc() + logging.exception(e) + except: + print("A fatal error occured!") + +# We need to catch all exceptions and return 0 in any case! +# If we do not return 0, login will FAIL FOR EVERYONE! +sys.exit(0) diff --git a/roles/lmn_printer/files/onLogout b/roles/lmn_printer/files/onLogout new file mode 100644 index 0000000..9fe8a00 --- /dev/null +++ b/roles/lmn_printer/files/onLogout @@ -0,0 +1,38 @@ +#!/usr/bin/python3 + +# DO NOT MODIFY THIS SCRIPT! +# For custom scripts use the hookdirs +# /etc/linuxmuster-linuxclient7/onLoginAsRoot.d +# and /etc/linuxmuster-linuxclient7/onLogoutAsRoot.d + +# This schript is called in root context when a user logs in or out +try: + import os, sys + from linuxmusterLinuxclient7 import logging, hooks, constants, user, shares, printers, computer, realm + + pamType = os.getenv("PAM_TYPE") + pamUser = os.getenv("PAM_USER") + #PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER and PAM_TYPE + logging.info("====== onLoginLogoutAsRoot started with PAM_TYPE={0} PAM_RHOST={1} PAM_RUSER={2} PAM_SERVICE={3} PAM_TTY={4} PAM_USER={5} ======".format(pamType, os.getenv("PAM_RHOST"), os.getenv("PAM_RUSER"), os.getenv("PAM_SERVICE"), os.getenv("PAM_TTY"), pamUser)) + + # check if whe should execute + if not hooks.shouldHooksBeExecuted(pamUser): + logging.info("======> onLoginLogoutAsRoot end ====") + sys.exit(0) + + elif pamType == "close_session": + # cleanup + printers.uninstallAllPrintersOfUser(pamUser) + + logging.info("======> onLoginLogoutAsRoot end ======") + +except Exception as e: + try: + logging.exception(e) + except: + print("A fatal error occured!") + +# We need to catch all exceptions and return 0 in any case! +# If we do not return 0, login will FAIL FOR EVERYONE! +sys.exit(0) + diff --git a/roles/lmn_printer/files/rmlpr.service b/roles/lmn_printer/files/rmlpr.service new file mode 100644 index 0000000..e89b994 --- /dev/null +++ b/roles/lmn_printer/files/rmlpr.service @@ -0,0 +1,6 @@ +[Unit] +Description=Remove all printers + +[Service] +Type=simple +ExecStart=sh -c 'lpstat -p && for printer in $(lpstat -p | cut -f 2 -d " "); do lpadmin -x $printer; done || echo no printer found.' diff --git a/roles/lmn_printer/files/rmlpr.timer b/roles/lmn_printer/files/rmlpr.timer new file mode 100644 index 0000000..a8097d2 --- /dev/null +++ b/roles/lmn_printer/files/rmlpr.timer @@ -0,0 +1,8 @@ +[Unit] +Description=Remove all printers on boot + +[Timer] +OnBootSec=10 + +[Install] +WantedBy=timers.target diff --git a/roles/lmn_printer/files/scripts/sudoTools b/roles/lmn_printer/files/scripts/sudoTools new file mode 100755 index 0000000..7459135 --- /dev/null +++ b/roles/lmn_printer/files/scripts/sudoTools @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +# +# Script to do some things that require root permissions as a normal user +# Currently used for: +# - mounting shares +# - installing printers +# + +import os, sys, argparse +from linuxmusterLinuxclient7 import shares, printers, constants + + +parser = argparse.ArgumentParser(description="Script to do some things that require root permissions as a normal user") + +subparsers = parser.add_subparsers(title='Tasks', metavar="", help="The task to execute", dest="task", required=True) + +subparserCache = subparsers.add_parser('install-printer', help='install a printer') +requiredGroupCache = subparserCache.add_argument_group('required arguments') +requiredGroupCache.add_argument("--path", help="The network path of the printer", required=True) +requiredGroupCache.add_argument("--name", help="The name of the printer", required=True) + +subparserCache = subparsers.add_parser('mount-share', help='mount a network share') +subparserCache.add_argument("--hidden", help="Hide this share", action='store_true') +requiredGroupCache = subparserCache.add_argument_group('required arguments') +requiredGroupCache.add_argument("--path", help="The network path of the share", required=True) +requiredGroupCache.add_argument("--name", help="The name of the share", required=True) + +args = parser.parse_args() + +task = args.task + +if not os.geteuid() == 0: + print("This script has to be run using sudo!") + exit(1) + +username = os.getenv("SUDO_USER") + +if task == "install-printer": + if printers.installPrinter(args.path, name=args.name, username=username): + sys.exit(0) + else: + sys.exit(1) + pass +elif task == "mount-share": + if shares._mountShare(username, args.path, args.name, args.hidden, False): + sys.exit(0) + else: + sys.exit(1) + +exit(0) \ No newline at end of file diff --git a/roles/lmn_printer/tasks/main.yml b/roles/lmn_printer/tasks/main.yml new file mode 100644 index 0000000..79f90b0 --- /dev/null +++ b/roles/lmn_printer/tasks/main.yml @@ -0,0 +1,102 @@ +--- +- name: Install cups and python libs + apt: + name: + - cups + - python3-ldap + state: latest + +- name: Disable cups printer browsing + lineinfile: + dest: /etc/cups/cupsd.conf + regexp: '^(Browsing ).*' + line: '\1No' + backrefs: yes + +- name: Disable cups-browsed + ansible.builtin.systemd: + name: cups-browsed.service + state: stopped + enabled: no + +- name: Configure pam_mount sysvol mount + blockinfile: + dest: /etc/security/pam_mount.conf.xml + marker: "" + block: | + rootansibleDebian-gdmsddmvirti + insertafter: "" + +- name: Create /etc/linuxmuster-linuxclient7 Directory + file: + path: /etc/linuxmuster-linuxclient7 + state: directory + mode: 0755 + +- name: install linuxmuster-linuxclient network.conf + template: + src: network.conf.j2 + dest: /etc/linuxmuster-linuxclient7/network.conf + mode: 0644 + +- name: install linuxmuster-linuxclient python libs + copy : + src: linuxmusterLinuxclient7 + dest: /usr/lib/python3/dist-packages + +- name: Create /usr/share/linuxmuster-linuxclient7/scripts Directory + file: + path: /usr/share/linuxmuster-linuxclient7/scripts + state: directory + mode: 0755 + +- name: install linuxmuster-scripts + copy: + src: scripts/sudoTools + dest: /usr/share/linuxmuster-linuxclient7/scripts/ + mode: 0755 + +- name: install lmn-sudotools + copy: + src: 90-lmn-sudotools + dest: /etc/sudoers.d/ + mode: 0660 + owner: root + group: root + +- name: install onLogin script + copy : + src: onLogin + dest: /usr/local/bin/ + mode: 0755 + owner: root + group: root + +- name: install lmn-printer.sh in /etc/profile.d/ + copy: + src: lmn-printer.sh + dest: /etc/profile.d/ + mode: 0644 + owner: root + group: root + +- name: Provide service and timer for remove all printers on boot + copy: + src: "{{ item }}" + dest: "/etc/systemd/system/{{ item }}" + mode: 0644 + with_items: + - rmlpr.service + - rmlpr.timer + +- name: enable rmlpr.timer + systemd: + name: rmlpr.timer + enabled: true + diff --git a/roles/lmn_printer/templates/network.conf.j2 b/roles/lmn_printer/templates/network.conf.j2 new file mode 100644 index 0000000..5cd39ce --- /dev/null +++ b/roles/lmn_printer/templates/network.conf.j2 @@ -0,0 +1,5 @@ +[network] +serverHostname = server +domain = pn.steinbeis.schule +realm = PN.STEINBEIS.SCHULE +version = 1