|
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(-) |