|
Line 0
Link Here
|
| 0 |
- |
1 |
#!/usr/bin/python2.7 |
|
|
2 |
# coding: utf-8 |
| 3 |
# |
| 4 |
# Univention Management Console module: |
| 5 |
# System Diagnosis UMC module |
| 6 |
# |
| 7 |
# Copyright 2017 Univention GmbH |
| 8 |
# |
| 9 |
# http://www.univention.de/ |
| 10 |
# |
| 11 |
# All rights reserved. |
| 12 |
# |
| 13 |
# The source code of this program is made available |
| 14 |
# under the terms of the GNU Affero General Public License version 3 |
| 15 |
# (GNU AGPL V3) as published by the Free Software Foundation. |
| 16 |
# |
| 17 |
# Binary versions of this program provided by Univention to you as |
| 18 |
# well as other copyrighted, protected or trademarked materials like |
| 19 |
# Logos, graphics, fonts, specific documentations and configurations, |
| 20 |
# cryptographic keys etc. are subject to a license agreement between |
| 21 |
# you and Univention and not subject to the GNU AGPL V3. |
| 22 |
# |
| 23 |
# In the case you use this program under the terms of the GNU AGPL V3, |
| 24 |
# the program is provided in the hope that it will be useful, |
| 25 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 26 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 27 |
# GNU Affero General Public License for more details. |
| 28 |
# |
| 29 |
# You should have received a copy of the GNU Affero General Public |
| 30 |
# License with the Debian GNU/Linux or Univention distribution in file |
| 31 |
# /usr/share/common-licenses/AGPL-3; if not, see |
| 32 |
# <http://www.gnu.org/licenses/>. |
| 33 |
|
| 34 |
import ldap |
| 35 |
import socket |
| 36 |
import pexpect |
| 37 |
import subprocess |
| 38 |
import itertools as it |
| 39 |
|
| 40 |
import univention.uldap |
| 41 |
import univention.admin.uldap |
| 42 |
import univention.admin.modules as udm_modules |
| 43 |
|
| 44 |
import univention.config_registry |
| 45 |
from univention.management.console.modules.diagnostic import Warning |
| 46 |
|
| 47 |
from univention.lib.i18n import Translation |
| 48 |
_ = Translation('univention-management-console-module-diagnostic').translate |
| 49 |
|
| 50 |
title = _('Check IMAP shared folder ACLs') |
| 51 |
description = _('All shared folder ACLs are in sync with UDM.') |
| 52 |
|
| 53 |
|
| 54 |
class ACLError(Exception): |
| 55 |
pass |
| 56 |
|
| 57 |
|
| 58 |
class MailboxNotExistentError(ACLError): |
| 59 |
def __init__(self, mailbox): |
| 60 |
super(ACLError, self).__init__(mailbox) |
| 61 |
self.mailbox = mailbox |
| 62 |
|
| 63 |
def __str__(self): |
| 64 |
msg = _('Mail folder {folder!r} does not exist in IMAP.') |
| 65 |
return msg.format(folder=self.mailbox) |
| 66 |
|
| 67 |
|
| 68 |
class ACLIdentifierError(Exception): |
| 69 |
def __init__(self, identifier): |
| 70 |
super(ACLIdentifierError, self).__init__(identifier) |
| 71 |
self.identifier = identifier |
| 72 |
|
| 73 |
|
| 74 |
class DuplicateIdentifierACLError(ACLIdentifierError): |
| 75 |
def __str__(self): |
| 76 |
msg = _('Multiple ACL entries for {id!r} in UDM.') |
| 77 |
return msg.format(id=self.identifier) |
| 78 |
|
| 79 |
|
| 80 |
class IdentifierWhitespaceACLError(ACLIdentifierError): |
| 81 |
def __str__(self): |
| 82 |
msg = _('Identifier with whitespace: {ids}. This is not supported by Cyrus.') |
| 83 |
return msg.format(ids=', '.join(repr(i) for i in self.identifier)) |
| 84 |
|
| 85 |
|
| 86 |
class ACLDifferenceError(ACLIdentifierError): |
| 87 |
def __init__(self, identifier, udm_right, actual_right): |
| 88 |
super(ACLDifferenceError, self).__init__(identifier) |
| 89 |
self.udm_right = udm_right |
| 90 |
self.actual_right = actual_right |
| 91 |
|
| 92 |
|
| 93 |
class UserACLError(ACLDifferenceError): |
| 94 |
def __str__(self): |
| 95 |
msg = _('ACL right for user {id!r} is {udm!r} in UDM, but {imap!r} in IMAP.') |
| 96 |
return msg.format(id=self.identifier, udm=self.udm_right, imap=self.actual_right) |
| 97 |
|
| 98 |
|
| 99 |
class GroupACLError(ACLDifferenceError): |
| 100 |
def __str__(self): |
| 101 |
msg = _('ACL right for group {id!r} is {udm!r} in UDM, but {imap!r} in IMAP.') |
| 102 |
return msg.format(id=self.identifier, udm=self.udm_right, imap=self.actual_right) |
| 103 |
|
| 104 |
|
| 105 |
class MailFolder(object): |
| 106 |
def __init__(self, udm_folder): |
| 107 |
self.dn = udm_folder.dn |
| 108 |
self.name = udm_folder.get('name') |
| 109 |
self.mail_domain = udm_folder.get('mailDomain') |
| 110 |
self.mail_address = udm_folder.get('mailPrimaryAddress') |
| 111 |
self._user_acl = udm_folder.get('sharedFolderUserACL') |
| 112 |
self._group_acl = udm_folder.get('sharedFolderGroupACL') |
| 113 |
|
| 114 |
@property |
| 115 |
def common_name(self): |
| 116 |
return '{}@{}'.format(self.name, self.mail_domain) |
| 117 |
|
| 118 |
def acl(self): |
| 119 |
return ACL.from_udm(self._user_acl, self._group_acl) |
| 120 |
|
| 121 |
@classmethod |
| 122 |
def from_udm(cls): |
| 123 |
univention.admin.modules.update() |
| 124 |
(ldap_connection, position) = univention.admin.uldap.getMachineConnection() |
| 125 |
module = udm_modules.get('mail/folder') |
| 126 |
for instance in module.lookup(None, ldap_connection, ''): |
| 127 |
instance.open() |
| 128 |
yield cls(instance) |
| 129 |
|
| 130 |
|
| 131 |
class ACL(object): |
| 132 |
RIGHTS = ('all', 'write', 'append', 'post', 'read', 'none') |
| 133 |
|
| 134 |
def __init__(self, user_acl, group_acl): |
| 135 |
self.user_acl = user_acl |
| 136 |
self.group_acl = group_acl |
| 137 |
|
| 138 |
@classmethod |
| 139 |
def from_udm(cls, user_acl, group_acl): |
| 140 |
''' |
| 141 |
Transform the udm acls from [[id, right], [id, right], ..] to a dict |
| 142 |
from identifier to right, where right is the highest right in the acl. |
| 143 |
''' |
| 144 |
def simplify(acl_list): |
| 145 |
merged = dict() |
| 146 |
for (identifier, right) in acl_list: |
| 147 |
merged.setdefault(identifier, set()).add(right) |
| 148 |
for (identifier, rights) in merged.iteritems(): |
| 149 |
if len(rights) > 1: |
| 150 |
raise DuplicateIdentifierACLError(identifier) |
| 151 |
else: |
| 152 |
udm_right = next(right for right in cls.RIGHTS if right in rights) |
| 153 |
yield (identifier, udm_right) |
| 154 |
return cls(dict(simplify(user_acl)), dict(simplify(group_acl))) |
| 155 |
|
| 156 |
def check_names_for_whitespace(self): |
| 157 |
identifier = self.user_acl.viewkeys() | self.group_acl.viewkeys() |
| 158 |
with_whitespace = [i for i in identifier if any(c.isspace() for c in i)] |
| 159 |
if with_whitespace: |
| 160 |
raise IdentifierWhitespaceACLError(with_whitespace) |
| 161 |
|
| 162 |
def difference(self, other): |
| 163 |
if isinstance(other, CyrusACL): |
| 164 |
self.check_names_for_whitespace() |
| 165 |
user_diff = self._diff(UserACLError, self.user_acl, other.user_acl) |
| 166 |
group_diff = self._diff(GroupACLError, self.group_acl, other.group_acl) |
| 167 |
return it.chain(user_diff, group_diff) |
| 168 |
|
| 169 |
def _diff(self, exception, expected, actual): |
| 170 |
all_id = expected.viewkeys() | actual.viewkeys() |
| 171 |
for identifier in all_id: |
| 172 |
exp = expected.get(identifier, 'none') |
| 173 |
act = actual.get(identifier, 'none') |
| 174 |
if exp != act: |
| 175 |
yield exception(identifier, exp, act) |
| 176 |
|
| 177 |
|
| 178 |
class DovecotACL(ACL): |
| 179 |
DOVECOT_RIGHT_TRANSLATION = ( |
| 180 |
('all', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert', |
| 181 |
'write-deleted', 'expunge', 'admin'))), |
| 182 |
('write', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert', |
| 183 |
'write-deleted', 'expunge'))), |
| 184 |
('append', set(('lookup', 'read', 'write', 'write-seen', 'post', 'insert'))), |
| 185 |
('post', set(('lookup', 'read', 'write', 'write-seen', 'post'))), |
| 186 |
('read', set(('lookup', 'read', 'write', 'write-seen'))), |
| 187 |
('none', set()), |
| 188 |
) |
| 189 |
|
| 190 |
@classmethod |
| 191 |
def from_folder(cls, folder): |
| 192 |
acl_list = cls._get_dovecot_acl(folder) |
| 193 |
merged = dict() |
| 194 |
for (identifier, rights) in acl_list.iteritems(): |
| 195 |
acl_type = 'group' if identifier.startswith('group=') else 'user' |
| 196 |
udm_id = identifier.replace('user=', '', 1) if identifier.startswith('user=') \ |
| 197 |
else identifier.replace('group=', '', 1) if identifier.startswith('group=') \ |
| 198 |
else identifier |
| 199 |
udm_right = next(udm_right for (udm_right, dovecot_rights) |
| 200 |
in cls.DOVECOT_RIGHT_TRANSLATION if rights.issuperset(dovecot_rights)) |
| 201 |
merged.setdefault(acl_type, dict())[udm_id] = udm_right |
| 202 |
return cls(merged.get('user', {}), merged.get('group', {})) |
| 203 |
|
| 204 |
@staticmethod |
| 205 |
def _get_dovecot_acl(folder): |
| 206 |
mailbox = 'shared/{pm}' if folder.mail_address else '{cn}/INBOX' |
| 207 |
cmd = ('doveadm', 'acl', 'get', '-u', 'Administrator', |
| 208 |
mailbox.format(cn=folder.common_name, pm=folder.mail_address)) |
| 209 |
output = subprocess.check_output(cmd, stderr=subprocess.PIPE).splitlines() |
| 210 |
return {identifier.strip(): set(rights.strip().split()) for (identifier, rights) |
| 211 |
in (line.rsplit(' ', 1) for line in output)} |
| 212 |
|
| 213 |
|
| 214 |
class CyrusACL(ACL): |
| 215 |
CYRUS_RIGHT_TRANSLATION = ( |
| 216 |
('all', set('lrswipcda')), |
| 217 |
('write', set('lrswipcd')), |
| 218 |
('append', set('lrsip')), |
| 219 |
('post', set('lrps')), |
| 220 |
('read', set('lrs')), |
| 221 |
('none', set()), |
| 222 |
) |
| 223 |
|
| 224 |
@classmethod |
| 225 |
def from_folder(cls, folder): |
| 226 |
acl_list = cls._get_cyrus_acl(folder) |
| 227 |
merged = dict() |
| 228 |
for (identifier, rights) in acl_list.iteritems(): |
| 229 |
acl_type = 'group' if identifier.startswith('group:') else 'user' |
| 230 |
udm_id = identifier.replace('user:', '', 1) if identifier.startswith('user:') \ |
| 231 |
else identifier.replace('group:', '', 1) if identifier.startswith('group:') \ |
| 232 |
else identifier |
| 233 |
udm_right = next(udm_right for (udm_right, cyrus_rights) |
| 234 |
in cls.CYRUS_RIGHT_TRANSLATION if rights.issuperset(cyrus_rights)) |
| 235 |
merged.setdefault(acl_type, dict())[udm_id] = udm_right |
| 236 |
return cls(merged.get('user', {}), merged.get('group', {})) |
| 237 |
|
| 238 |
@staticmethod |
| 239 |
def _get_cyrus_acl(folder): |
| 240 |
configRegistry = univention.config_registry.ConfigRegistry() |
| 241 |
configRegistry.load() |
| 242 |
|
| 243 |
hostname = configRegistry.get('mail/cyrus/murder/backend/hostname', 'localhost') |
| 244 |
with open('/etc/cyrus.secret') as fob: |
| 245 |
password = fob.read().rstrip() |
| 246 |
|
| 247 |
cyradm = pexpect.spawn(' '.join(('cyradm', '-u', 'cyrus', hostname))) |
| 248 |
cyradm.setecho(False) |
| 249 |
index = cyradm.expect(['IMAP Password:', '(cannot connect)|(unknown host)', |
| 250 |
pexpect.EOF], timeout=60) |
| 251 |
if index == 0: |
| 252 |
cyradm.sendline(password) |
| 253 |
cyradm.expect('>') |
| 254 |
cyradm.sendline('listacl shared/{}'.format(folder.common_name)) |
| 255 |
cyradm.sendline('disconnect') |
| 256 |
cyradm.sendline('exit') |
| 257 |
line_generator = (cyradm.readline().strip() for _ in it.repeat(None)) |
| 258 |
acl_lines = list(it.takewhile(lambda l: l and not l.endswith('cyradm>'), |
| 259 |
line_generator)) |
| 260 |
if 'Mailbox does not exist' in acl_lines: |
| 261 |
raise MailboxNotExistentError(folder.common_name) |
| 262 |
return {identifier.strip(): set(rights.strip()) for (identifier, rights) |
| 263 |
in (line.split() for line in acl_lines)} |
| 264 |
return dict() |
| 265 |
|
| 266 |
|
| 267 |
def all_differences(acl_class): |
| 268 |
for folder in MailFolder.from_udm(): |
| 269 |
try: |
| 270 |
udm_acl = folder.acl() |
| 271 |
imap_acl = acl_class.from_folder(folder) |
| 272 |
for difference in udm_acl.difference(imap_acl): |
| 273 |
yield (folder, difference) |
| 274 |
except ACLError as error: |
| 275 |
yield (folder, error) |
| 276 |
|
| 277 |
|
| 278 |
def udm_mail_link(folder): |
| 279 |
return { |
| 280 |
'module': 'udm', |
| 281 |
'flavor': 'mail/mail', |
| 282 |
'props': { |
| 283 |
'openObject': { |
| 284 |
'objectDN': folder.dn, |
| 285 |
'objectType': 'mail/folder' |
| 286 |
} |
| 287 |
} |
| 288 |
} |
| 289 |
|
| 290 |
|
| 291 |
def is_service_active(service): |
| 292 |
lo = univention.uldap.getMachineConnection() |
| 293 |
raw_filter = '(&(univentionService=%s)(cn=%s))' |
| 294 |
filter_expr = ldap.filter.filter_format(raw_filter, (service, socket.gethostname())) |
| 295 |
for (dn, _attr) in lo.search(filter_expr, attr=['cn']): |
| 296 |
if dn is not None: |
| 297 |
return True |
| 298 |
return False |
| 299 |
|
| 300 |
|
| 301 |
def run(): |
| 302 |
if not is_service_active('IMAP'): |
| 303 |
return |
| 304 |
|
| 305 |
configRegistry = univention.config_registry.ConfigRegistry() |
| 306 |
configRegistry.load() |
| 307 |
|
| 308 |
if configRegistry.is_true('mail/cyrus'): |
| 309 |
acl_class = CyrusACL |
| 310 |
elif configRegistry.is_true('mail/dovecot'): |
| 311 |
acl_class = DovecotACL |
| 312 |
else: |
| 313 |
return |
| 314 |
|
| 315 |
differerces = list(all_differences(acl_class)) |
| 316 |
ed = [_('Found differences in the ACLs for IMAP shared folders between UDM and IMAP.') + ' ' + |
| 317 |
_('This is not necessarily a problem, if the the ACL got changed via IMAP.')] |
| 318 |
|
| 319 |
modules = list() |
| 320 |
for (folder, group) in it.groupby(differerces, lambda x: x[0]): |
| 321 |
name = folder.common_name |
| 322 |
ed.append('') |
| 323 |
ed.append(_('In mail folder {name} (see {{udm:mail/mail}}):').format(name=name)) |
| 324 |
ed.extend(str(error) for (_, error) in group) |
| 325 |
modules.append(udm_mail_link(folder)) |
| 326 |
|
| 327 |
# XXX Extra Text if multiple |
| 328 |
if modules: |
| 329 |
raise Warning(description='\n'.join(ed), umc_modules=modules) |
| 330 |
|
| 331 |
|
| 332 |
if __name__ == '__main__': |
| 333 |
run() |
| 334 |
from univention.management.console.modules.diagnostic import main |
| 335 |
main() |
| 1 |
(po) |
336 |
(po) |
| 2 |
-- |
|
|
| 3 |
.../umc/python/diagnostic/de.po | 56 +++++++++++++++++++++- |
337 |
.../umc/python/diagnostic/de.po | 56 +++++++++++++++++++++- |
| 4 |
1 file changed, 54 insertions(+), 2 deletions(-) |
338 |
1 file changed, 54 insertions(+), 2 deletions(-) |