View | Details | Raw Unified | Return to bug 40465
Collapse All | Expand All

(-)a/management/univention-management-console-module-diagnostic/debian/control (-2 / +1 lines)
 Lines 17-22   Depends: ${misc:Depends}, Link Here 
17
 python-pycurl,
17
 python-pycurl,
18
 python-psutil,
18
 python-psutil,
19
 python-dnspython,
19
 python-dnspython,
20
 python-pexpect,
20
 python-paramiko
21
 python-paramiko
21
Description: System Diagnosis UMC module
22
Description: System Diagnosis UMC module
22
 .
23
 .
23
- 
24
--
25
.../umc/python/diagnostic/plugins/mail_acl_sync.py | 335 +++++++++++++++++++++
24
.../umc/python/diagnostic/plugins/mail_acl_sync.py | 335 +++++++++++++++++++++
26
1 file changed, 335 insertions(+)
25
1 file changed, 335 insertions(+)
27
create mode 100755 management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py
26
create mode 100755 management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py
(-)a/management/univention-management-console-module-diagnostic/umc/python/diagnostic/plugins/mail_acl_sync.py (-2 / +335 lines)
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(-)
(-)a/management/univention-management-console-module-diagnostic/umc/python/diagnostic/de.po (-3 / +54 lines)
 Lines 2-9    Link Here 
2
msgid ""
2
msgid ""
3
msgstr ""
3
msgstr ""
4
"Project-Id-Version: univention-management-console-module-diagnostic\n"
4
"Project-Id-Version: univention-management-console-module-diagnostic\n"
5
"Report-Msgid-Bugs-To: packages@univention.de\n"
5
"Report-Msgid-Bugs-To: \n"
6
"POT-Creation-Date: 2016-01-14 12:19+0100\n"
6
"POT-Creation-Date: 2017-06-26 13:45+0200\n"
7
"PO-Revision-Date: \n"
7
"PO-Revision-Date: \n"
8
"Last-Translator: Univention GmbH <packages@univention.de>\n"
8
"Last-Translator: Univention GmbH <packages@univention.de>\n"
9
"Language-Team: Univention GmbH <packages@univention.de>\n"
9
"Language-Team: Univention GmbH <packages@univention.de>\n"
 Lines 23-32   msgstr "" Link Here 
23
"Eine Zeitüberschreitung trat beim Erreichen des Nameservers auf (ist er "
23
"Eine Zeitüberschreitung trat beim Erreichen des Nameservers auf (ist er "
24
"online?)."
24
"online?)."
25
25
26
#: umc/python/diagnostic/plugins/mail_acl_sync.py:101
27
msgid "ACL right for group {id!r} is {udm!r} in UDM, but {imap!r} in IMAP."
28
msgstr ""
29
"ACL Berechtigung für die Gruppe {id!r} ist {udm!r} in UDM, aber {imap!r} in "
30
"IMAP."
31
32
#: umc/python/diagnostic/plugins/mail_acl_sync.py:95
33
msgid "ACL right for user {id!r} is {udm!r} in UDM, but {imap!r} in IMAP."
34
msgstr ""
35
"ACL Berechtigung für den Nutzer {id!r} ist {udm!r} in UDM, aber {imap!r} in "
36
"IMAP."
37
26
#: umc/python/diagnostic/plugins/security_limits.py:31
38
#: umc/python/diagnostic/plugins/security_limits.py:31
27
msgid "Adjust to suggested limits"
39
msgid "Adjust to suggested limits"
28
msgstr "An vorgeschlagene Limits anpassen"
40
msgstr "An vorgeschlagene Limits anpassen"
29
41
42
#: umc/python/diagnostic/plugins/mail_acl_sync.py:51
43
msgid "All shared folder ACLs are in sync with UDM."
44
msgstr "Alle ACL Einträge der globalen IMAP Ordner stimmen mit UDM überein."
45
46
#: umc/python/diagnostic/plugins/mail_acl_sync.py:50
47
msgid "Check IMAP shared folder ACLs"
48
msgstr "Überprüfe ACL Einträge der globalen IMAP Ordner"
49
50
#: umc/python/diagnostic/plugins/mail_acl_sync.py:316
51
msgid ""
52
"Found differences in the ACLs for IMAP shared folders between UDM and IMAP."
53
msgstr ""
54
"Es wurde ein Unterschied in den ACL Einträge für die globalen IMAP Ordner "
55
"zwischen UDM und IMAP gefunden."
56
30
#: umc/python/diagnostic/plugins/gateway.py:11
57
#: umc/python/diagnostic/plugins/gateway.py:11
31
msgid "Gateway is not reachable"
58
msgid "Gateway is not reachable"
32
msgstr "Gateway ist nicht erreichbar"
59
msgstr "Gateway ist nicht erreichbar"
 Lines 35-40   msgstr "Gateway ist nicht erreichbar" Link Here 
35
msgid "Host key for server does not match"
62
msgid "Host key for server does not match"
36
msgstr "SSH Host-Key des Servers stimmt nicht überein"
63
msgstr "SSH Host-Key des Servers stimmt nicht überein"
37
64
65
#: umc/python/diagnostic/plugins/mail_acl_sync.py:82
66
#, python-brace-format
67
msgid "Identifier with whitespace: {ids}. This is not supported by Cyrus."
68
msgstr ""
69
"Es gibt Namen mit Leerzeichen: {ids}. Dies wird nicht von Cyrus unterstützt."
70
38
#: umc/python/diagnostic/plugins/nameserver.py:17
71
#: umc/python/diagnostic/plugins/nameserver.py:17
39
msgid ""
72
msgid ""
40
"If the problem persists make sure the nameserver is connected to the network "
73
"If the problem persists make sure the nameserver is connected to the network "
 Lines 49-54   msgid "If these settings are correct the problem relies in the gateway itself:" Link Here 
49
msgstr ""
82
msgstr ""
50
"Wenn diese Einstellungen richtig sind liegt das Problem im Gateway selbst:"
83
"Wenn diese Einstellungen richtig sind liegt das Problem im Gateway selbst:"
51
84
85
#: umc/python/diagnostic/plugins/mail_acl_sync.py:323
86
#, python-brace-format
87
msgid "In mail folder {name} (see {{udm:mail/mail}}):"
88
msgstr "Im globalen Mail Ordner {name} (siehe {{udm:mail/mail}}):"
89
52
#: umc/python/diagnostic/plugins/security_limits.py:19
90
#: umc/python/diagnostic/plugins/security_limits.py:19
53
#, python-brace-format
91
#, python-brace-format
54
msgid ""
92
msgid ""
 Lines 80-85   msgstr "" Link Here 
80
msgid "Machine authentication failed"
118
msgid "Machine authentication failed"
81
msgstr "Authentifizierung mit dem Maschinen-Konto ist fehlgeschlagen"
119
msgstr "Authentifizierung mit dem Maschinen-Konto ist fehlgeschlagen"
82
120
121
#: umc/python/diagnostic/plugins/mail_acl_sync.py:64
122
msgid "Mail folder {folder!r} does not exist in IMAP."
123
msgstr "Globaler IMAP Order {folder!r} existiert nicht in IMAP."
124
83
#: umc/python/diagnostic/plugins/gateway.py:15
125
#: umc/python/diagnostic/plugins/gateway.py:15
84
msgid "Make sure the hardware of the gateway device is working properly."
126
msgid "Make sure the hardware of the gateway device is working properly."
85
msgstr ""
127
msgstr ""
 Lines 93-98   msgstr "" Link Here 
93
"Weitere Informationen über die Ursache können durch Ausführen von \"dpkg --"
135
"Weitere Informationen über die Ursache können durch Ausführen von \"dpkg --"
94
"audit\" erhalten werden."
136
"audit\" erhalten werden."
95
137
138
#: umc/python/diagnostic/plugins/mail_acl_sync.py:76
139
msgid "Multiple ACL entries for {id!r} in UDM."
140
msgstr "Mehrere ACL Einträge für {id!r} in UDM."
141
96
#: umc/python/diagnostic/plugins/nameserver.py:13
142
#: umc/python/diagnostic/plugins/nameserver.py:13
97
msgid "Nameserver(s) are not responsive"
143
msgid "Nameserver(s) are not responsive"
98
msgstr "Nameserver sind nicht ansprechbar"
144
msgstr "Nameserver sind nicht ansprechbar"
 Lines 249-254   msgstr "" Link Here 
249
"an Samba-Servern unmöglich, Dateioperationen (Kopieren, Verschieben) auf "
295
"an Samba-Servern unmöglich, Dateioperationen (Kopieren, Verschieben) auf "
250
"Freigaben kann fehlschlagen, uvm.)"
296
"Freigaben kann fehlschlagen, uvm.)"
251
297
298
#: umc/python/diagnostic/plugins/mail_acl_sync.py:317
299
msgid "This is not necessarily a problem, if the the ACL got changed via IMAP."
300
msgstr ""
301
"Dies ist nicht notwendigerweise ein Problem, wenn die ACL Einträge per IMAP "
302
"geändert wurden."
303
252
#: umc/python/diagnostic/plugins/proxy.py:83
304
#: umc/python/diagnostic/plugins/proxy.py:83
253
#, python-format
305
#, python-format
254
msgid ""
306
msgid ""
255
- 

Return to bug 40465