This repository has been archived by the owner on Jul 30, 2019. It is now read-only.
forked from it25gmbh/plucs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
plucs-groups.py
executable file
·432 lines (334 loc) · 12.2 KB
/
plucs-groups.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# -*- coding: utf-8 -*-
#
# PLUCS (XMPP integration for UCS)
"""PLUCS GROUPS listener module."""
#
# Copyright 2013-2014 it25 GmbH
#
# http://www.it25.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.
#
# 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
# <http://www.gnu.org/licenses/>.
# pylint: disable-msg=C0103,W0704
__package__ = '' # workaround for PEP 366
import listener
import univention.debug as ud
import univention.config_registry
import os
import io
import re
import marshal
name = 'plucs-groups'
description = 'Converts group lists into eJabberD ACLs, and group properties to eJabberD access rules.'
filter = '(|(objectClass=univentionXMPPGroup)(objectClass=univentionXMPPAccount))'
attributes = ['cn','memberUid','uid','xmppEnabled','xmppMessageGroups','xmppPresenceGroups']
# set to true for every change
changed = False
# track 'need-to-be-initialized' (read cache needed)
initialized = False
# set on clean(), cleared on first save_data()
cleaned = False
# static storage of groups and users currently known
_dumpfile = os.path.join('/var','cache','plucs',name + '.cache')
_cache = {
'groups': {}, # key=group name (sanitized), value=array of member uids
'gname': {}, # key=group name (sanitized), value=real group name
'users': [], # uids of all XMPP-enabled users
'r_mesg': {}, # key=group name (sanitized), value=array of san. group names (groups to talk to)
'r_pres': {} # key=group name (sanitized), value=array of san. group names (groups to see presence)
}
def initialize():
"""Initialize the module once on first start or after clean."""
ud.debug(ud.LISTENER, ud.INFO, 'plucs-groups: initialize() called')
global initialized
if _read_saved_data():
_write_groups_config()
_write_rights_config()
ud.debug(ud.LISTENER, ud.PROCESS, 'plucs-groups: initialized')
initialized = True
def handler(dn, new, old):
"""Handle changes to 'dn'."""
global initialized
global changed
ud.debug(ud.LISTENER, ud.INFO, "plucs-groups: DN '%s' changed" % dn)
if (not initialized):
ud.debug(ud.LISTENER, ud.PROCESS, "plucs-groups: initializing on first invocation.")
initialize()
# handle changes to users
if (dn[0:4] == 'uid='):
# user appeared or changed?
if (new):
enab = (new.get('xmppEnabled')[0] == 'TRUE')
uid = new.get('uid')[0]
if enab:
_add_user(uid)
else:
_del_user(uid)
# user removed?
else:
uid = old.get('uid')[0]
_del_user(uid)
changed = True
# handle changes to groups
elif (dn[0:3] == 'cn='):
# modified groups are always removed and readded, this covers all kinds
# of change (modrdn, xmppEnabled, members, permission lists).
enab = False
if (old):
grpn = _sane_groupname(old.get('cn')[0])
_del_group(grpn)
if (new):
gn = new.get('cn')[0]
grpn = _sane_groupname(gn)
enab = (new.get('xmppEnabled')[0] == 'TRUE')
if enab:
r_mesg = map(_sane_groupname,new.get('xmppMessageGroups',[]))
r_pres = map(_sane_groupname,new.get('xmppPresenceGroups',[]))
_add_group(grpn,gn,new.get('memberUid',[]),r_mesg,r_pres)
changed = True
# should never happen
else:
ud.debug(ud.LISTENER, ud.WARN,"plucs-groups: don't know how to handle DN '%s'" % dn)
_write_groups_config()
_write_rights_config()
_save_data()
def clean():
"""Handle request to clean-up the module."""
global _cache
global cleaned
ud.debug(ud.LISTENER, ud.PROCESS, "plucs-groups: cleanup requested.")
try:
_cache = {
'groups': {},
'gname': {},
'users': [],
'r_mesg': {},
'r_pres': {}
}
os.unlink(_dumpfile)
cleaned = True
except:
pass
def postrun():
"""handle changes after at least 15s of no-changes"""
ud.debug(ud.LISTENER, ud.INFO, "postrun: plucs-groups running")
global changed
if not changed:
ud.debug(ud.LISTENER, ud.INFO, "plucs-groups: nothing changed, not restarting daemon.")
return
changed = False
ud.debug(ud.LISTENER, ud.PROCESS, "plucs-groups: reloading ACLs (%d users, %d groups)" % (len(_cache['users']),len(_cache['groups'].keys())))
# TODO: set UID 0 and run the to-be-found-command to reload ACLs into an Erlang node
# Currently: reload eJabberd.
try:
listener.run('/usr/sbin/invoke-rc.d', ['invoke-rc.d', 'plucs', 'restart'], uid=0)
except Exception, e:
ud.debug(ud.ADMIN, ud.WARN, 'The restart of the PLUCS server failed: %s' % str(e))
# --------------- helper functions ------------------
def _write_groups_config():
"""write our static data into the config file (mod_filter_groups.cfg)."""
fn = '/etc/ejabberd/mod_filter_groups.cfg'
txt = ''
txt += u"%% ---------------------------------------------------------\n"
txt += u"%% Group ACL file for eJabberD (PLUCS) XMPP service\n"
txt += u"%% Please don't edit this file by hand, it will be overwritten\n"
txt += u"%% by the next change to any XMPP-enabled group/user.\n"
txt += u"%% ---------------------------------------------------------\n\n"
for g in _cache['groups'].keys():
if g in _cache['groups']:
utemp = []
for u in _cache['groups'][g]:
if u in _cache['users']:
utemp.append(u)
if len(utemp):
txt += u"%% ------- %s ------\n" % _cache['gname'][g]
for u in utemp:
txt += u"{acl, %s, {user, \"%s\"}}.\n" % (g,u)
_safe_write(fn,txt)
def _write_rights_config():
"""write our static data into the config file (mod_filter.cfg).
Uses UCR variables for the policies to be applied.
"""
ucr = univention.config_registry.ConfigRegistry()
ucr.load()
fn = '/etc/ejabberd/mod_filter.cfg'
txt = u''
txt += u"%% ---------------------------------------------------------\n"
txt += u"%% Access rules file for eJabberD (PLUCS) XMPP service\n"
txt += u"%% Please don't edit this file by hand, it will be overwritten\n"
txt += u"%% by the next change to any XMPP-enabled group/user.\n"
txt += u"%% ---------------------------------------------------------\n\n"
# empty permission lists for all known sender groups
mesg_dict = {}
pres_dict = {}
for g in _cache['groups'].keys():
pres_dict[g] = []
mesg_dict[g] = []
for g in _cache['groups'].keys():
txt += u"%% Group: '%s' (%s)\n" % (g,_cache['gname'][g])
# collect 'message' permissions
if ('r_mesg' in _cache) and (g in _cache['r_mesg']):
for tg in _cache['r_mesg'][g]:
if tg in _cache['groups']:
txt += u"%% Message -> [%s]\n" % tg
mesg_dict[g].append(tg)
else:
txt += u"%% Message -> [%s] (ignored)\n" % tg
# collect 'presence' permissions
if ('r_pres' in _cache) and (g in _cache['r_pres']):
if len(_cache['r_pres'][g]):
for tg in _cache['r_pres'][g]:
if tg in _cache['groups']:
txt += u"%% Presence <- [%s]\n" % tg
pres_dict[tg].append(g)
else:
txt += u"%% Presence <- [%s] (ignored)\n" % tg
else:
if (ucr.get('plucs/mod/filter/presence_policy','deny') == 'allow'):
txt += u"% -- applying 'allow' policy for presence --\n"
for tg in _cache['groups'].keys():
txt += u"%% Presence -< [%s]\n" % tg
pres_dict[tg].append(g)
txt += u"\n{access, mod_filter, [{allow, all}]}.\n\n"
pol = ucr.get('plucs/mod/filter/message_policy','deny')
ud.debug(ud.LISTENER, ud.INFO, "plucs-groups: applying policy [%s] to MESSAGE stanzas" % pol)
txt += _config_tuple(mesg_dict,'mod_filter_message','message_',pol)
txt += _config_tuple(pres_dict,'mod_filter_presence','presence_')
txt += "{access, mod_filter_iq, [{allow, all}]}.\n\n"
_safe_write(fn,txt)
# ----------------- users -------------------
def _add_user(uid):
"""add the given user to our internal list"""
if not uid in _cache['users']:
_cache['users'].append(uid)
def _del_user(uid):
"""remove the given user from our internal list"""
if uid in _cache['users']:
_cache['users'].remove(uid)
# -------------- groups ----------------------
def _add_group(grp,name,users,mesg,pres):
"""add (or update / overwrite) a group"""
global _cache
_cache['groups'][grp] = users
_cache['gname'][grp] = name
if not 'r_mesg' in _cache:
_cache['r_mesg'] = {}
_cache['r_mesg'][grp] = mesg
if not 'r_pres' in _cache:
_cache['r_pres'] = {}
_cache['r_pres'][grp] = pres
def _del_group(grp):
"""remove a group"""
for key in ['groups','gname','r_mesg','r_pres']:
if key in _cache:
if grp in _cache[key].keys():
del _cache[key][grp]
def _sane_groupname(grp):
"""Sanitize a (possibly UTF-8) group name for use as an Erlang symbol"""
#g = grp.casefold() # Python >=3.3 only!
g = grp.lower()
g = re.sub('[^a-z0-9]+','_',g)
g = g.strip('_')
return g
# --------------- cache ----------------------
def _read_saved_data():
"""Read saved arrays _users and _groups into memory"""
global _cache
# if 'clear()' was called recently -> it is no error if the dump file doesn't exist
if cleaned:
if not os.path.exists(_dumpfile):
return True
try:
f = open(_dumpfile,'rb')
_cache = marshal.load(f)
f.close()
ud.debug(ud.LISTENER,ud.PROCESS,"plucs-groups: read %d users, %d groups from cache" % (len(_cache['users']),len(_cache['groups'].keys())))
return True
except Exception,e:
ud.debug(ud.LISTENER, ud.WARN,
"%s: could not read dump file %s: %s" % (name, _dumpfile, str(e)))
return False
def _save_data():
"""Saves arrays _users and _groups across restarts"""
global cleaned
# ensure alert on nonexistant dump file on next _read_saved_data()
cleaned = False
try:
f = open(_dumpfile,'wb')
marshal.dump(_cache,f)
f.close()
ud.debug(ud.LISTENER,ud.INFO,"plucs-groups: cached %d users, %d groups" % (len(_cache['users']),len(_cache['groups'].keys())))
except Exception,e:
ud.debug(ud.LISTENER, ud.ERROR,
"%s: could not write dump file %s: %s" % (name, _dumpfile, str(e)))
# ---------------------- optimization -----------------
def _config_tuple(dict,name,prefix,pol='deny'):
"""Returns a snippet of Erlang config items, built from the 'dict' dictionary,
suitable as access rules.
dict dictionary format:
key = source group name
value = array of destination group names to be allowed (can be empty)
name:
the name of the access rule
prefix:
the string to prepend onto a group name -> name of associated sub-rule
pol:
the policy (fallback rule for 'no match') for the first-level
access rule (if a group doesn't have any groups set for a given
permission)
All group names are already sanitized, we don't refer to any other
data but the 'dict' passed in.
"""
result = u"{access, %s, [\n" % name
other_rules = u""
for src in dict.keys():
if len(dict[src]):
result += u"\t{%s%s, %s},\n" % (prefix,src,src)
rule = u"{access, %s%s, [\n" % (prefix,src)
for dst in dict[src]:
rule += u"\t{allow, %s},\n" % dst
rule += u"\t{deny, all}\n"
rule += u"]}.\n"
other_rules += rule
result += u"\t{%s, all}\n" % pol
result += u"]}.\n"
return (result + other_rules + u"\n")
def _safe_write(fname,content):
"""Safe write to a file.
(1) create a temp file, write content.
(2) rename to real destination name.
Sets identity to root.
Error behaviour:
- catches all exceptions
- writes log here
To return true/false on success/failure doesn't make sense since our callers
don't bother looking at our results.
"""
bakfile = fname + '.tmp'
listener.setuid(0)
try:
f = io.open(bakfile,'w',encoding='utf-8')
f.write(content)
f.close()
ud.debug(ud.LISTENER,ud.INFO,"plucs-groups: File '%s' written (%d chars)" % (bakfile,len(content)))
os.rename(bakfile,fname)
ud.debug(ud.LISTENER,ud.INFO,"plucs-groups: File '%s' renamed to '%s'" % (bakfile,fname))
except Exception,e:
ud.debug(ud.LISTENER,ud.WARN,"plucs-groups: Could not write '%s': %s" % (bakfile,str(e)))
finally:
f.close()
listener.unsetuid()