From b4140ee800ab2f43baab047b17bc784a9b59e5b6 Mon Sep 17 00:00:00 2001 From: Lukas Oyen Date: Mon, 26 Jun 2017 13:23:01 +0200 Subject: [PATCH 1/3] Bug #40465: umc-diagnostic: add python-pexpect --- .../univention-management-console-module-diagnostic/debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/management/univention-management-console-module-diagnostic/debian/control b/management/univention-management-console-module-diagnostic/debian/control index a2e98d1..23b4892 100644 --- a/management/univention-management-console-module-diagnostic/debian/control +++ b/management/univention-management-console-module-diagnostic/debian/control @@ -17,6 +17,7 @@ Depends: ${misc:Depends}, python-pycurl, python-psutil, python-dnspython, + python-pexpect, python-paramiko Description: System Diagnosis UMC module . -- 2.7.4 From 6bf299059d982714882b68298daf965d3c589219 Mon Sep 17 00:00:00 2001 From: Lukas Oyen Date: Tue, 20 Jun 2017 14:31:46 +0200 Subject: [PATCH 2/3] Bug #40465: umc-diagnostic: new check mail_acl_sync.py --- .../umc/python/diagnostic/plugins/mail_acl_sync.py | 335 +++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100755 management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py diff --git a/management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py b/management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py new file mode 100755 index 0000000..1bb25a3 --- /dev/null +++ b/management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py @@ -0,0 +1,335 @@ +#!/usr/bin/python2.7 +# coding: utf-8 +# +# Univention Management Console module: +# System Diagnosis UMC module +# +# Copyright 2017 Univention GmbH +# +# http://www.univention.de/ +# +# All rights reserved. +# +# The source code of this program is made available +# under the terms of the GNU Affero General Public License version 3 +# (GNU AGPL V3) as published by the Free Software Foundation. +# +# Binary versions of this program provided by Univention to you as +# well as other copyrighted, protected or trademarked materials like +# Logos, graphics, fonts, specific documentations and configurations, +# cryptographic keys etc. are subject to a license agreement between +# you and Univention and not subject to the GNU AGPL V3. +# +# In the case you use this program under the terms of the GNU AGPL V3, +# the program is provided in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License with the Debian GNU/Linux or Univention distribution in file +# /usr/share/common-licenses/AGPL-3; if not, see +# . + +import ldap +import socket +import pexpect +import subprocess +import itertools as it + +import univention.uldap +import univention.admin.uldap +import univention.admin.modules as udm_modules + +import univention.config_registry +from univention.management.console.modules.diagnostic import Warning + +from univention.lib.i18n import Translation +_ = Translation('univention-management-console-module-diagnostic').translate + +title = _('Check IMAP shared folder ACLs') +description = _('All shared folder ACLs are in sync with UDM.') + + +class ACLError(Exception): + pass + + +class MailboxNotExistentError(ACLError): + def __init__(self, mailbox): + super(ACLError, self).__init__(mailbox) + self.mailbox = mailbox + + def __str__(self): + msg = _('Mail folder {folder!r} does not exist in IMAP.') + return msg.format(folder=self.mailbox) + + +class ACLIdentifierError(Exception): + def __init__(self, identifier): + super(ACLIdentifierError, self).__init__(identifier) + self.identifier = identifier + + +class DuplicateIdentifierACLError(ACLIdentifierError): + def __str__(self): + msg = _('Multiple ACL entries for {id!r} in UDM.') + return msg.format(id=self.identifier) + + +class IdentifierWhitespaceACLError(ACLIdentifierError): + def __str__(self): + msg = _('Identifier with whitespace: {ids}. This is not supported by Cyrus.') + return msg.format(ids=', '.join(repr(i) for i in self.identifier)) + + +class ACLDifferenceError(ACLIdentifierError): + def __init__(self, identifier, udm_right, actual_right): + super(ACLDifferenceError, self).__init__(identifier) + self.udm_right = udm_right + self.actual_right = actual_right + + +class UserACLError(ACLDifferenceError): + def __str__(self): + msg = _('ACL right for user {id!r} is {udm!r} in UDM, but {imap!r} in IMAP.') + return msg.format(id=self.identifier, udm=self.udm_right, imap=self.actual_right) + + +class GroupACLError(ACLDifferenceError): + def __str__(self): + msg = _('ACL right for group {id!r} is {udm!r} in UDM, but {imap!r} in IMAP.') + return msg.format(id=self.identifier, udm=self.udm_right, imap=self.actual_right) + + +class MailFolder(object): + def __init__(self, udm_folder): + self.dn = udm_folder.dn + self.name = udm_folder.get('name') + self.mail_domain = udm_folder.get('mailDomain') + self.mail_address = udm_folder.get('mailPrimaryAddress') + self._user_acl = udm_folder.get('sharedFolderUserACL') + self._group_acl = udm_folder.get('sharedFolderGroupACL') + + @property + def common_name(self): + return '{}@{}'.format(self.name, self.mail_domain) + + def acl(self): + return ACL.from_udm(self._user_acl, self._group_acl) + + @classmethod + def from_udm(cls): + univention.admin.modules.update() + (ldap_connection, position) = univention.admin.uldap.getMachineConnection() + module = udm_modules.get('mail/folder') + for instance in module.lookup(None, ldap_connection, ''): + instance.open() + yield cls(instance) + + +class ACL(object): + RIGHTS = ('all', 'write', 'append', 'post', 'read', 'none') + + def __init__(self, user_acl, group_acl): + self.user_acl = user_acl + self.group_acl = group_acl + + @classmethod + def from_udm(cls, user_acl, group_acl): + ''' + Transform the udm acls from [[id, right], [id, right], ..] to a dict + from identifier to right, where right is the highest right in the acl. + ''' + def simplify(acl_list): + merged = dict() + for (identifier, right) in acl_list: + merged.setdefault(identifier, set()).add(right) + for (identifier, rights) in merged.iteritems(): + if len(rights) > 1: + raise DuplicateIdentifierACLError(identifier) + else: + udm_right = next(right for right in cls.RIGHTS if right in rights) + yield (identifier, udm_right) + return cls(dict(simplify(user_acl)), dict(simplify(group_acl))) + + def check_names_for_whitespace(self): + identifier = self.user_acl.viewkeys() | self.group_acl.viewkeys() + with_whitespace = [i for i in identifier if any(c.isspace() for c in i)] + if with_whitespace: + raise IdentifierWhitespaceACLError(with_whitespace) + + def difference(self, other): + if isinstance(other, CyrusACL): + self.check_names_for_whitespace() + user_diff = self._diff(UserACLError, self.user_acl, other.user_acl) + group_diff = self._diff(GroupACLError, self.group_acl, other.group_acl) + return it.chain(user_diff, group_diff) + + def _diff(self, exception, expected, actual): + all_id = expected.viewkeys() | actual.viewkeys() + for identifier in all_id: + exp = expected.get(identifier, 'none') + act = actual.get(identifier, 'none') + if exp != act: + yield exception(identifier, exp, act) + + +class DovecotACL(ACL): + DOVECOT_RIGHT_TRANSLATION = ( + ('all', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert', + 'write-deleted', 'expunge', 'admin'))), + ('write', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert', + 'write-deleted', 'expunge'))), + ('append', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert'))), + ('post', set(('lookup', 'read', 'write', 'write-seen', 'post'))), + ('read', set(('lookup', 'read', 'write', 'write-seen'))), + ('none', set()), + ) + + @classmethod + def from_folder(cls, folder): + acl_list = cls._get_dovecot_acl(folder) + merged = dict() + for (identifier, rights) in acl_list.iteritems(): + acl_type = 'group' if identifier.startswith('group=') else 'user' + udm_id = identifier.replace('user=', '', 1) if identifier.startswith('user=') \ + else identifier.replace('group=', '', 1) if identifier.startswith('group=') \ + else identifier + udm_right = next(udm_right for (udm_right, dovecot_rights) + in cls.DOVECOT_RIGHT_TRANSLATION if rights.issuperset(dovecot_rights)) + merged.setdefault(acl_type, dict())[udm_id] = udm_right + return cls(merged.get('user', {}), merged.get('group', {})) + + @staticmethod + def _get_dovecot_acl(folder): + mailbox = 'shared/{pm}' if folder.mail_address else '{cn}/INBOX' + cmd = ('doveadm', 'acl', 'get', '-u', 'Administrator', + mailbox.format(cn=folder.common_name, pm=folder.mail_address)) + output = subprocess.check_output(cmd, stderr=subprocess.PIPE).splitlines() + return {identifier.strip(): set(rights.strip().split()) for (identifier, rights) + in (line.rsplit(' ', 1) for line in output)} + + +class CyrusACL(ACL): + CYRUS_RIGHT_TRANSLATION = ( + ('all', set('lrswipcda')), + ('write', set('lrswipcd')), + ('append', set('lrsip')), + ('post', set('lrps')), + ('read', set('lrs')), + ('none', set()), + ) + + @classmethod + def from_folder(cls, folder): + acl_list = cls._get_cyrus_acl(folder) + merged = dict() + for (identifier, rights) in acl_list.iteritems(): + acl_type = 'group' if identifier.startswith('group:') else 'user' + udm_id = identifier.replace('user:', '', 1) if identifier.startswith('user:') \ + else identifier.replace('group:', '', 1) if identifier.startswith('group:') \ + else identifier + udm_right = next(udm_right for (udm_right, cyrus_rights) + in cls.CYRUS_RIGHT_TRANSLATION if rights.issuperset(cyrus_rights)) + merged.setdefault(acl_type, dict())[udm_id] = udm_right + return cls(merged.get('user', {}), merged.get('group', {})) + + @staticmethod + def _get_cyrus_acl(folder): + configRegistry = univention.config_registry.ConfigRegistry() + configRegistry.load() + + hostname = configRegistry.get('mail/cyrus/murder/backend/hostname', 'localhost') + with open('/etc/cyrus.secret') as fob: + password = fob.read().rstrip() + + cyradm = pexpect.spawn(' '.join(('cyradm', '-u', 'cyrus', hostname))) + cyradm.setecho(False) + index = cyradm.expect(['IMAP Password:', '(cannot connect)|(unknown host)', + pexpect.EOF], timeout=60) + if index == 0: + cyradm.sendline(password) + cyradm.expect('>') + cyradm.sendline('listacl shared/{}'.format(folder.common_name)) + cyradm.sendline('disconnect') + cyradm.sendline('exit') + line_generator = (cyradm.readline().strip() for _ in it.repeat(None)) + acl_lines = list(it.takewhile(lambda l: l and not l.endswith('cyradm>'), + line_generator)) + if 'Mailbox does not exist' in acl_lines: + raise MailboxNotExistentError(folder.common_name) + return {identifier.strip(): set(rights.strip()) for (identifier, rights) + in (line.split() for line in acl_lines)} + return dict() + + +def all_differences(acl_class): + for folder in MailFolder.from_udm(): + try: + udm_acl = folder.acl() + imap_acl = acl_class.from_folder(folder) + for difference in udm_acl.difference(imap_acl): + yield (folder, difference) + except ACLError as error: + yield (folder, error) + + +def udm_mail_link(folder): + return { + 'module': 'udm', + 'flavor': 'mail/mail', + 'props': { + 'openObject': { + 'objectDN': folder.dn, + 'objectType': 'mail/folder' + } + } + } + + +def is_service_active(service): + lo = univention.uldap.getMachineConnection() + raw_filter = '(&(univentionService=%s)(cn=%s))' + filter_expr = ldap.filter.filter_format(raw_filter, (service, socket.gethostname())) + for (dn, _attr) in lo.search(filter_expr, attr=['cn']): + if dn is not None: + return True + return False + + +def run(): + if not is_service_active('IMAP'): + return + + configRegistry = univention.config_registry.ConfigRegistry() + configRegistry.load() + + if configRegistry.is_true('mail/cyrus'): + acl_class = CyrusACL + elif configRegistry.is_true('mail/dovecot'): + acl_class = DovecotACL + else: + return + + differerces = list(all_differences(acl_class)) + ed = [_('Found differences in the ACLs for IMAP shared folders between UDM and IMAP.') + ' ' + + _('This is not necessarily a problem, if the the ACL got changed via IMAP.')] + + modules = list() + for (folder, group) in it.groupby(differerces, lambda x: x[0]): + name = folder.common_name + ed.append('') + ed.append(_('In mail folder {name} (see {{udm:mail/mail}}):').format(name=name)) + ed.extend(str(error) for (_, error) in group) + modules.append(udm_mail_link(folder)) + + # XXX Extra Text if multiple + if modules: + raise Warning(description='\n'.join(ed), umc_modules=modules) + + +if __name__ == '__main__': + run() + from univention.management.console.modules.diagnostic import main + main() -- 2.7.4 From 3b9d3376bbdb2ecf2902c214013fbe3169b96f5f Mon Sep 17 00:00:00 2001 From: Lukas Oyen Date: Mon, 26 Jun 2017 13:18:17 +0200 Subject: [PATCH 3/3] Bug #40465: umc-diagnostic: new check mail_acl_sync.py (po) --- .../umc/python/diagnostic/de.po | 56 +++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/management/univention-management-console-module-diagnostic/umc/python/diagnostic/de.po b/management/univention-management-console-module-diagnostic/umc/python/diagnostic/de.po index affad86..bcdad1d 100644 --- a/management/univention-management-console-module-diagnostic/umc/python/diagnostic/de.po +++ b/management/univention-management-console-module-diagnostic/umc/python/diagnostic/de.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: univention-management-console-module-diagnostic\n" -"Report-Msgid-Bugs-To: packages@univention.de\n" -"POT-Creation-Date: 2016-01-14 12:19+0100\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-26 13:45+0200\n" "PO-Revision-Date: \n" "Last-Translator: Univention GmbH \n" "Language-Team: Univention GmbH \n" @@ -23,10 +23,37 @@ msgstr "" "Eine Zeitüberschreitung trat beim Erreichen des Nameservers auf (ist er " "online?)." +#: umc/python/diagnostic/plugins/mail_acl_sync.py:101 +msgid "ACL right for group {id!r} is {udm!r} in UDM, but {imap!r} in IMAP." +msgstr "" +"ACL Berechtigung für die Gruppe {id!r} ist {udm!r} in UDM, aber {imap!r} in " +"IMAP." + +#: umc/python/diagnostic/plugins/mail_acl_sync.py:95 +msgid "ACL right for user {id!r} is {udm!r} in UDM, but {imap!r} in IMAP." +msgstr "" +"ACL Berechtigung für den Nutzer {id!r} ist {udm!r} in UDM, aber {imap!r} in " +"IMAP." + #: umc/python/diagnostic/plugins/security_limits.py:31 msgid "Adjust to suggested limits" msgstr "An vorgeschlagene Limits anpassen" +#: umc/python/diagnostic/plugins/mail_acl_sync.py:51 +msgid "All shared folder ACLs are in sync with UDM." +msgstr "Alle ACL Einträge der globalen IMAP Ordner stimmen mit UDM überein." + +#: umc/python/diagnostic/plugins/mail_acl_sync.py:50 +msgid "Check IMAP shared folder ACLs" +msgstr "Überprüfe ACL Einträge der globalen IMAP Ordner" + +#: umc/python/diagnostic/plugins/mail_acl_sync.py:316 +msgid "" +"Found differences in the ACLs for IMAP shared folders between UDM and IMAP." +msgstr "" +"Es wurde ein Unterschied in den ACL Einträge für die globalen IMAP Ordner " +"zwischen UDM und IMAP gefunden." + #: umc/python/diagnostic/plugins/gateway.py:11 msgid "Gateway is not reachable" msgstr "Gateway ist nicht erreichbar" @@ -35,6 +62,12 @@ msgstr "Gateway ist nicht erreichbar" msgid "Host key for server does not match" msgstr "SSH Host-Key des Servers stimmt nicht überein" +#: umc/python/diagnostic/plugins/mail_acl_sync.py:82 +#, python-brace-format +msgid "Identifier with whitespace: {ids}. This is not supported by Cyrus." +msgstr "" +"Es gibt Namen mit Leerzeichen: {ids}. Dies wird nicht von Cyrus unterstützt." + #: umc/python/diagnostic/plugins/nameserver.py:17 msgid "" "If the problem persists make sure the nameserver is connected to the network " @@ -49,6 +82,11 @@ msgid "If these settings are correct the problem relies in the gateway itself:" msgstr "" "Wenn diese Einstellungen richtig sind liegt das Problem im Gateway selbst:" +#: umc/python/diagnostic/plugins/mail_acl_sync.py:323 +#, python-brace-format +msgid "In mail folder {name} (see {{udm:mail/mail}}):" +msgstr "Im globalen Mail Ordner {name} (siehe {{udm:mail/mail}}):" + #: umc/python/diagnostic/plugins/security_limits.py:19 #, python-brace-format msgid "" @@ -80,6 +118,10 @@ msgstr "" msgid "Machine authentication failed" msgstr "Authentifizierung mit dem Maschinen-Konto ist fehlgeschlagen" +#: umc/python/diagnostic/plugins/mail_acl_sync.py:64 +msgid "Mail folder {folder!r} does not exist in IMAP." +msgstr "Globaler IMAP Order {folder!r} existiert nicht in IMAP." + #: umc/python/diagnostic/plugins/gateway.py:15 msgid "Make sure the hardware of the gateway device is working properly." msgstr "" @@ -93,6 +135,10 @@ msgstr "" "Weitere Informationen über die Ursache können durch Ausführen von \"dpkg --" "audit\" erhalten werden." +#: umc/python/diagnostic/plugins/mail_acl_sync.py:76 +msgid "Multiple ACL entries for {id!r} in UDM." +msgstr "Mehrere ACL Einträge für {id!r} in UDM." + #: umc/python/diagnostic/plugins/nameserver.py:13 msgid "Nameserver(s) are not responsive" msgstr "Nameserver sind nicht ansprechbar" @@ -249,6 +295,12 @@ msgstr "" "an Samba-Servern unmöglich, Dateioperationen (Kopieren, Verschieben) auf " "Freigaben kann fehlschlagen, uvm.)" +#: umc/python/diagnostic/plugins/mail_acl_sync.py:317 +msgid "This is not necessarily a problem, if the the ACL got changed via IMAP." +msgstr "" +"Dies ist nicht notwendigerweise ein Problem, wenn die ACL Einträge per IMAP " +"geändert wurden." + #: umc/python/diagnostic/plugins/proxy.py:83 #, python-format msgid "" -- 2.7.4