· 7 years ago · Nov 05, 2018, 01:46 PM
1# -*- coding: iso-8859-1 -*-
2
3# Copyright 2002-2016 University of Oslo, Norway
4#
5# This file is part of Cerebrum.
6#
7# Cerebrum is free software; you can redistribute it and/or modify it
8# under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# Cerebrum is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Cerebrum; if not, write to the Free Software Foundation,
19# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20
21import time
22import re
23import imaplib
24import ssl
25import pickle
26import socket
27
28from mx import DateTime
29from flanker.addresslib import address as email_validator
30
31import cereconf
32from Cerebrum import Database
33from Cerebrum import Entity
34from Cerebrum import Errors
35from Cerebrum import Metainfo
36from Cerebrum.Constants import _LanguageCode
37from Cerebrum import Utils
38from Cerebrum.modules import Email
39from Cerebrum.modules.pwcheck.checker import (check_password,
40 PasswordNotGoodEnough,
41 RigidPasswordNotGoodEnough,
42 PhrasePasswordNotGoodEnough)
43from Cerebrum.modules.pwcheck.history import PasswordHistory
44from Cerebrum.modules.bofhd.bofhd_core import BofhdCommonMethods
45from Cerebrum.modules.bofhd.cmd_param import *
46from Cerebrum.modules.bofhd.errors import CerebrumError, PermissionDenied
47from Cerebrum.modules.bofhd.utils import BofhdRequests
48from Cerebrum.modules.bofhd.auth import (BofhdAuthOpSet,
49 AuthConstants,
50 BofhdAuthOpTarget,
51 BofhdAuthRole)
52from Cerebrum.modules.bofhd.help import Help
53from Cerebrum.modules.no import fodselsnr
54from Cerebrum.modules.bofhd import bofhd_core_help
55from Cerebrum.modules.no.uit.bofhd_auth import BofhdAuth
56from Cerebrum.modules.no.uit.access_FS import FS
57from Cerebrum.modules.no.uit.DiskQuota import DiskQuota
58from Cerebrum.modules.dns.Subnet import Subnet
59
60
61# TBD: It would probably be cleaner if our time formats were specified
62# in a non-Java-SimpleDateTime-specific way.
63def format_day(field):
64 fmt = "yyyy-MM-dd" # 10 characters wide
65 return ":".join((field, "date", fmt))
66
67
68def format_time(field):
69 fmt = "yyyy-MM-dd HH:mm" # 16 characters wide
70 return ':'.join((field, "date", fmt))
71
72
73def date_to_string(date):
74 """Takes a DateTime-object and formats a standard ISO-datestring
75 from it.
76
77 Custom-made for our purposes, since the standard XMLRPC-libraries
78 restrict formatting to years after 1899, and we see years prior to
79 that.
80
81 """
82 if not date:
83 return "<not set>"
84
85 return "%04i-%02i-%02i" % (date.year, date.month, date.day)
86
87
88class TimeoutException(Exception):
89 pass
90
91
92class ConnectException(Exception):
93 pass
94
95
96class RTQueue(Parameter):
97 _type = 'rtQueue'
98 _help_ref = 'rt_queue'
99
100
101# TODO: move more UiO cruft from bofhd/auth.py in here
102class UiOAuth(BofhdAuth):
103 """Authorisation. UiO specific operations and business logic."""
104
105 def can_rt_create(self, operator, domain=None, query_run_any=False):
106 if self.is_superuser(operator, query_run_any):
107 return True
108 if query_run_any:
109 return self._has_operation_perm_somewhere(operator,
110 self.const.auth_rt_create)
111 return self._query_maildomain_permissions(operator,
112 self.const.auth_rt_create,
113 domain, None)
114
115 can_rt_delete = can_rt_create
116
117 def can_rt_address_add(self, operator, domain=None, query_run_any=False):
118 if self.is_superuser(operator, query_run_any):
119 return True
120 if query_run_any:
121 return self._has_operation_perm_somewhere(operator,
122 self.const.auth_rt_addr_add)
123 return self._query_maildomain_permissions(operator,
124 self.const.auth_rt_addr_add,
125 domain, None)
126
127 can_rt_address_remove = can_rt_address_add
128
129
130class BofhdExtension(BofhdCommonMethods):
131 """All CallableFuncs take user as first arg, and are responsible
132 for checking necessary permissions"""
133
134 all_commands = {}
135 hidden_commands = {}
136 omit_parent_commands = {'user_create'}
137 parent_commands = True
138
139 authz = UiOAuth
140 external_id_mappings = {}
141
142 # This little class is used to store connections to the LDAP servers, and
143 # the LDAP modules needed. The reason for doing things like this instead
144 # instead of importing the LDAP module for the entire bofhd_uio_cmds,
145 # are amongst others:
146 # 1. bofhd_uio_cmds is partially used at other institutions in some form,
147 # they might not have any need for, or wish, to install the LDAP module.
148 # 2. If we import the module on a per-function basis, we'll loose options
149 # set in the module.
150 # 3. It looks better to define a little class, than a dict of dicts, in
151 # order to organize the variables in a somewhat sane way.
152 #
153 # We need to connect to LDAP, in order to populate entries with the
154 # 'mailPause' attribute. This attribute will be heavily used by the
155 # postmasters, as they convert to murder. When we populate entries
156 # with the 'mailPause' attribute directly, the postmasters will experience
157 # a 3x reduction in waiting time.
158 #
159 # This stuff is used in _ldap_init(), _ldap_modify() and _ldap_delete(),
160 # which are called from email_pause().
161
162 class LDAPStruct:
163 ldap = None
164 ldapobject = None
165 connection = None
166
167 def invalidate_connection(self):
168 self.connection = None
169
170 _ldap_connect = LDAPStruct()
171
172 def __init__(self, *args, **kwargs):
173 super(BofhdExtension, self).__init__(*args, **kwargs)
174 self.external_id_mappings['fnr'] = self.const.externalid_fodselsnr
175 # exchange-relatert-jazz
176 # currently valid language variants for UiO-Cerebrum
177 # although these codes are used for distribution groups
178 # they are not directly related to them. maybe these should be
179 # put in a cereconf-variable somewhere in the future? (Jazz, 2013-12)
180 self.language_codes = ['nb', 'nn', 'en']
181
182 # TODO: Wait until needed / fix on import?
183 self.fixup_imaplib()
184
185 @property
186 def name_codes(self):
187 # TODO: Do we really need this cache?
188 try:
189 return self.__name_codes
190 except AttributeError:
191 self.__name_codes = dict()
192 person = Utils.Factory.get('Person')(self.db)
193 for t in person.list_person_name_codes():
194 self.__name_codes[int(t['code'])] = t['description']
195 return self.__name_codes
196
197 @property
198 def change_type2details(self):
199 # TODO: Do we really need this cache?
200 try:
201 return self.__ct2details
202 except AttributeError:
203 self.__ct2details = dict()
204 for r in self.db.get_changetypes():
205 self.__ct2details[int(r['change_type_id'])] = [
206 r['category'], r['type'], r['msg_string']]
207 return self.__ct2details
208
209 @property
210 def num2op_set_name(self):
211 # TODO: Do we really need this cache?
212 try:
213 return self.__num2opset
214 except AttributeError:
215 self.__num2opset = dict()
216 aos = BofhdAuthOpSet(self.db)
217 for r in aos.list():
218 self.__num2opset[int(r['op_set_id'])] = r['name']
219 return self.__num2opset
220
221 def fixup_imaplib(self):
222 def nonblocking_open(self, host=None, port=None):
223 import socket
224 # Perhaps using **kwargs is cleaner, but this works, too.
225 if host is None:
226 if not hasattr(self, "host"):
227 self.host = ''
228 else:
229 self.host = host
230 if port is None:
231 if not hasattr(self, "port"):
232 self.port = 143
233 else:
234 self.port = port
235
236 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
237 self.sock.setblocking(False)
238 err = self.sock.connect_ex((self.host, self.port))
239 # I don't think connect_ex() can ever return success immediately,
240 # it has to wait for a roundtrip.
241 assert err
242 if err != errno.EINPROGRESS:
243 raise ConnectException(errno.errorcode[err])
244
245 ignore, wset, ignore = select.select([], [self.sock], [], 1.0)
246 if not wset:
247 raise TimeoutException
248 err = self.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
249 if err == 0:
250 self.sock.setblocking(True)
251 self.file = self.sock.makefile('rb')
252 return
253 raise ConnectException(errno.errorcode[err])
254 setattr(imaplib.IMAP4, 'open', nonblocking_open)
255
256 @classmethod
257 def get_help_strings(cls):
258 return bofhd_core_help.get_help_strings()
259
260 @classmethod
261 def list_commands(cls, attr):
262 u""" Fetch all commands in all superclasses. """
263 commands = super(BofhdExtension, cls).list_commands(attr)
264 if attr == 'all_commands':
265 from Cerebrum.modules.dns.bofhd_dns_cmds import BofhdExtension as Dns
266 # FIXME: This hack is needed until we have a proper architecture
267 # for bofhd which allows mixins.
268 # We know that the format suggestion in dns has no hdr, so we only
269 # copy str_vars.
270 commands['host_info'] = Command(
271 ("host", "info"),
272 SimpleString(help_ref='string_host'),
273 YesNo(optional=True, help_ref='show_policy'),
274 fs=FormatSuggestion(Dns.all_commands['host_info'].get_fs()['str_vars'] +
275 [("Hostname: %s\n"
276 "Description: %s",
277 ("hostname", "desc")),
278 ("Default disk quota: %d MiB",
279 ("def_disk_quota",))]))
280 return commands
281
282 def _ldap_unbind(self):
283 ld = self._ldap_connect.connection
284 if ld:
285 try:
286 ld.unbind_s()
287 except self._ldap_connect.ldap.LDAPError:
288 pass
289 self._ldap_connect.connection = None
290
291 def _ldap_init(self):
292 """This helper function connects and binds to LDAP-servers
293 specified in cereconf."""
294 if self._ldap_connect.connection == None:
295 # We import here, as not everyone got LDAP.
296 try:
297 import ldap
298 from ldap import ldapobject
299 except ImportError:
300 raise CerebrumError, ('ldap module could not be imported')
301
302 # Store the LDAP module in a LDAPStruct, this way we'll keep the
303 # options between functions. These options are lost if we import
304 # the module for each function that uses it.
305 self._ldap_connect.ldap = ldap
306 self._ldap_connect.ldapobject = ldapobject
307 self._ldap_connect.__del__ = self._ldap_unbind
308
309 # Read the password and create the binddn
310 passwd = self.db._read_password(cereconf.LDAP_SYSTEM,
311 cereconf.LDAP_UPDATE_USER)
312 ld_binddn = cereconf.LDAP_BIND_DN % cereconf.LDAP_UPDATE_USER
313
314 # Avoid indefinite blocking
315 self._ldap_connect.ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 4)
316
317 # Require TLS cert. This option should be set in
318 # /etc/openldap/ldap.conf along with the cert itself,
319 # but let us make sure.
320 self._ldap_connect.ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
321 ldap.OPT_X_TLS_DEMAND)
322
323 server = cereconf.LDAP_MASTER
324
325 con = ldapobject.ReconnectLDAPObject("ldaps://%s/" % server,
326 retry_max = \
327 cereconf.LDAP_RETRY_MAX,
328 retry_delay = \
329 cereconf.LDAP_RETRY_DELAY)
330
331
332
333 try:
334 con.simple_bind_s(who=ld_binddn, cred=passwd)
335 except ldap.CONFIDENTIALITY_REQUIRED:
336 self.logger.warn('TLS could not be established to %s' % server)
337 raise CerebrumError, ('TLS could not be established to %s' % \
338 server)
339 except ldap.INVALID_CREDENTIALS:
340 rep_str = 'Connection aborted to %s, invalid credentials' \
341 % server
342 self.logger.error(rep_str)
343 raise CerebrumError, (rep_str)
344 except ldap.SERVER_DOWN:
345 con = None
346
347 # And we store the connection in our LDAPStruct
348 self._ldap_connect.connection = con
349
350
351
352 def _ldap_modify(self, dn, attribute, *values):
353 """This function modifies an LDAP entry defined by 'id' to contain an
354 attribute with the given values, or to delete it if no values."""
355
356 tries = 0
357 while tries < 2:
358 if not self._ldap_connect.connection:
359 self._ldap_init()
360 tries = 1
361 tries += 1
362 ld = self._ldap_connect.connection
363 if not ld:
364 break
365
366 # We'll set the trait on one server, and it should spread
367 # to the other servers in less than two miuntes. This
368 # eliminates race conditions when servers go up and down..
369 try:
370 ld.modify_s(dn, [(self._ldap_connect.ldap.MOD_REPLACE,
371 attribute, values or None)])
372 return True
373
374 except self._ldap_connect.ldap.NO_SUCH_OBJECT:
375 # This error occurs if the mail-target has been created
376 # and mailPause is being set before the newest LDIF has
377 # been handed over to LDAP.
378 break
379 except self._ldap_connect.ldap.SERVER_DOWN:
380 # We invalidate the connection (set it to None).
381 self._ldap_connect.invalidate_connection()
382
383 return False
384
385 #
386 # access commands
387 #
388
389 # access disk <path>
390 all_commands['access_disk'] = Command(
391 ('access', 'disk'),
392 DiskId(),
393 fs=FormatSuggestion("%-16s %-9s %s",
394 ("opset", "type", "name"),
395 hdr="%-16s %-9s %s" %
396 ("Operation set", "Type", "Name")))
397 def access_disk(self, operator, path):
398 disk = self._get_disk(path)[0]
399 result = []
400 host = Utils.Factory.get('Host')(self.db)
401 try:
402 host.find(disk.host_id)
403 for r in self._list_access("host", host.name, empty_result=[]):
404 if r['attr'] == '' or re.search("/%s$" % r['attr'], path):
405 result.append(r)
406 except Errors.NotFoundError:
407 pass
408 result.extend(self._list_access("disk", path, empty_result=[]))
409 return result or "None"
410
411 # access group <group>
412 all_commands['access_group'] = Command(
413 ('access', 'group'),
414 GroupName(),
415 fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
416 hdr="%-16s %-9s %s" %
417 ("Operation set", "Type", "Name")))
418 def access_group(self, operator, group):
419 return self._list_access("group", group)
420
421 # access host <hostname>
422 all_commands['access_host'] = Command(
423 ('access', 'host'),
424 SimpleString(help_ref="string_host"),
425 fs=FormatSuggestion("%-16s %-16s %-9s %s",
426 ("opset", "attr", "type", "name"),
427 hdr="%-16s %-16s %-9s %s" %
428 ("Operation set", "Pattern", "Type", "Name")))
429 def access_host(self, operator, host):
430 return self._list_access("host", host)
431
432 # access maildom <maildom>
433 all_commands['access_maildom'] = Command(
434 ('access', 'maildom'),
435 SimpleString(help_ref="email_domain"),
436 fs=FormatSuggestion("%-16s %-9s %s",
437 ("opset", "type", "name"),
438 hdr="%-16s %-9s %s" %
439 ("Operation set", "Type", "Name")))
440 def access_maildom(self, operator, maildom):
441 return self._list_access("maildom", maildom)
442
443 # access ou <ou>
444 all_commands['access_ou'] = Command(
445 ('access', 'ou'),
446 OU(),
447 fs=FormatSuggestion("%-16s %-16s %-9s %s",
448 ("opset", "attr", "type", "name"),
449 hdr="%-16s %-16s %-9s %s" %
450 ("Operation set", "Affiliation", "Type", "Name")))
451 def access_ou(self, operator, ou):
452 return self._list_access("ou", ou)
453
454 # access user <account>
455 all_commands['access_user'] = Command(
456 ('access', 'user'),
457 AccountName(),
458 fs=FormatSuggestion("%-14s %-5s %-20s %-7s %-9s %s",
459 ("opset", "target_type", "target", "attr",
460 "type", "name"),
461 hdr="%-14s %-5s %-20s %-7s %-9s %s" %
462 ("Operation set", "TType", "Target", "Attr",
463 "Type", "Name")))
464 def access_user(self, operator, user):
465 """This is more tricky than the others, we want to show anyone
466 with access, through OU, host or disk. (not global_XXX,
467 though.)
468
469 Note that there is no auth-type 'account', so you can't be
470 granted direct access to a specific user."""
471
472 acc = self._get_account(user)
473 # Make lists of the disks and hosts associated with the user
474 disks = {}
475 hosts = {}
476 disk = Utils.Factory.get("Disk")(self.db)
477 for r in acc.get_homes():
478 # Disk for archived users may not exist anymore
479 try:
480 disk_id = int(r['disk_id'])
481 except TypeError:
482 continue
483 if not disk_id in disks:
484 disk.clear()
485 disk.find(disk_id)
486 disks[disk_id] = disk.path
487 if disk.host_id is not None:
488 basename = disk.path.split("/")[-1]
489 host_id = int(disk.host_id)
490 if host_id not in hosts:
491 hosts[host_id] = []
492 hosts[host_id].append(basename)
493 # Look through disks
494 ret = []
495 for d in disks.keys():
496 for entry in self._list_access("disk", d, empty_result=[]):
497 entry['target_type'] = "disk"
498 entry['target'] = disks[d]
499 ret.append(entry)
500 # Look through hosts:
501 for h in hosts.keys():
502 for candidate in self._list_access("host", h, empty_result=[]):
503 candidate['target_type'] = "host"
504 candidate['target'] = self._get_host(h).name
505 if candidate['attr'] == "":
506 ret.append(candidate)
507 continue
508 for dir in hosts[h]:
509 if re.match(candidate['attr'], dir):
510 ret.append(candidate)
511 break
512 # TODO: check user's ou(s)
513 ret.sort(lambda x,y: (cmp(x['opset'].lower(), y['opset'].lower()) or
514 cmp(x['name'], y['name'])))
515 return ret
516
517 # access global_group
518 all_commands['access_global_group'] = Command(
519 ('access', 'global_group'),
520 fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
521 hdr="%-16s %-9s %s" %
522 ("Operation set", "Type", "Name")))
523 def access_global_group(self, operator):
524 return self._list_access("global_group")
525
526 # access global_host
527 all_commands['access_global_host'] = Command(
528 ('access', 'global_host'),
529 fs=FormatSuggestion("%-16s %-9s %s",
530 ("opset", "type", "name"),
531 hdr="%-16s %-9s %s" %
532 ("Operation set", "Type", "Name")))
533 def access_global_host(self, operator):
534 return self._list_access("global_host")
535
536 # access global_maildom
537 all_commands['access_global_maildom'] = Command(
538 ('access', 'global_maildom'),
539 fs=FormatSuggestion("%-16s %-9s %s",
540 ("opset", "type", "name"),
541 hdr="%-16s %-9s %s" %
542 ("Operation set", "Type", "Name")))
543 def access_global_maildom(self, operator):
544 return self._list_access("global_maildom")
545
546 # access global_ou
547 all_commands['access_global_ou'] = Command(
548 ('access', 'global_ou'),
549 fs=FormatSuggestion("%-16s %-16s %-9s %s",
550 ("opset", "attr", "type", "name"),
551 hdr="%-16s %-16s %-9s %s" %
552 ("Operation set", "Affiliation", "Type", "Name")))
553 def access_global_ou(self, operator):
554 return self._list_access("global_ou")
555
556 # access global_dns
557 all_commands['access_global_dns'] = Command(
558 ('access', 'global_dns'),
559 fs=FormatSuggestion("%-16s %-16s %-9s %s",
560 ("opset", "attr", "type", "name"),
561 hdr="%-16s %-16s %-9s %s" %
562 ("Operation set", "Affiliation", "Type", "Name")))
563 def access_global_dns(self, operator):
564 return self._list_access("global_dns")
565
566 def _list_access(self, target_type, target_name=None, decode_attr=str,
567 empty_result="None"):
568 target_id, target_type, target_auth = \
569 self._get_access_id(target_type, target_name)
570 ret = []
571 ar = BofhdAuthRole(self.db)
572 aos = BofhdAuthOpSet(self.db)
573 for r in self._get_auth_op_target(target_id, target_type,
574 any_attr=True):
575 if r['attr'] is None:
576 attr = ""
577 else:
578 attr = decode_attr(r['attr'])
579 for r2 in ar.list(op_target_id=r['op_target_id']):
580 aos.clear()
581 aos.find(r2['op_set_id'])
582 ety = self._get_entity(ident=r2['entity_id'])
583 ret.append({'opset': aos.name,
584 'attr': attr,
585 'type': str(self.const.EntityType(ety.entity_type)),
586 'name': self._get_name_from_object(ety)})
587 ret.sort(lambda a,b: (cmp(a['opset'], b['opset']) or
588 cmp(a['name'], b['name'])))
589 return ret or empty_result
590
591
592 # access grant <opset name> <who> <type> <on what> [<attr>]
593 all_commands['access_grant'] = Command(
594 ('access', 'grant'),
595 OpSet(),
596 GroupName(help_ref="id:target:group"),
597 EntityType(default='group', help_ref="auth_entity_type"),
598 SimpleString(help_ref="auth_target_entity"),
599 SimpleString(optional=True, help_ref="auth_attribute"),
600 perm_filter='can_grant_access')
601 def access_grant(self, operator, opset, group, entity_type, target_name,
602 attr=None):
603 return self._manipulate_access(self._grant_auth, operator, opset,
604 group, entity_type, target_name, attr)
605
606 # access revoke <opset name> <who> <type> <on what> [<attr>]
607 all_commands['access_revoke'] = Command(
608 ('access', 'revoke'),
609 OpSet(),
610 GroupName(help_ref="id:target:group"),
611 EntityType(default='group', help_ref="auth_entity_type"),
612 SimpleString(help_ref="auth_target_entity"),
613 SimpleString(optional=True, help_ref="auth_attribute"),
614 perm_filter='can_grant_access')
615 def access_revoke(self, operator, opset, group, entity_type, target_name,
616 attr=None):
617 return self._manipulate_access(self._revoke_auth, operator, opset,
618 group, entity_type, target_name, attr)
619
620 def _manipulate_access(self, change_func, operator, opset, group,
621 entity_type, target_name, attr):
622 """This function does no validation of types itself. It uses
623 _get_access_id() to get a (target_type, entity_id) suitable for
624 insertion in auth_op_target. Additional checking for validity
625 is done by _validate_access().
626
627 Those helper functions look for a function matching the
628 target_type, and call it. There should be one
629 _get_access_id_XXX and one _validate_access_XXX for each known
630 target_type.
631
632 """
633 opset = self._get_opset(opset)
634 gr = self.util.get_target(group, default_lookup="group",
635 restrict_to=['Account', 'Group'])
636 target_id, target_type, target_auth = \
637 self._get_access_id(entity_type, target_name)
638 operator_id = operator.get_entity_id()
639 if target_auth is None and not self.ba.is_superuser(operator_id):
640 raise PermissionDenied("Currently limited to superusers")
641 else:
642 self.ba.can_grant_access(operator_id, target_auth,
643 target_type, target_id, opset)
644 self._validate_access(entity_type, opset, attr)
645 return change_func(gr.entity_id, opset, target_id, target_type, attr,
646 group, target_name)
647
648 def _get_access_id(self, target_type, target_name):
649 """Get required data for granting access to an operation target.
650
651 :param str target_type: The type of
652
653 :rtype: tuple
654 :returns:
655 A three element tuple with information about the operation target:
656
657 1. The entity_id of the target entity (int)
658 2. The target type (str)
659 3. The `intval` of the operation constant for granting access to
660 the given target entity.
661
662 """
663 func_name = "_get_access_id_%s" % target_type
664 if not func_name in dir(self):
665 raise CerebrumError, "Unknown id type %s" % target_type
666 return self.__getattribute__(func_name)(target_name)
667
668 def _validate_access(self, target_type, opset, attr):
669 func_name = "_validate_access_%s" % target_type
670 if not func_name in dir(self):
671 raise CerebrumError, "Unknown type %s" % target_type
672 return self.__getattribute__(func_name)(opset, attr)
673
674 def _get_access_id_disk(self, target_name):
675 return (self._get_disk(target_name)[1],
676 self.const.auth_target_type_disk,
677 self.const.auth_grant_disk)
678 def _validate_access_disk(self, opset, attr):
679 # TODO: check if the opset is relevant for a disk
680 if attr is not None:
681 raise CerebrumError, "Can't specify attribute for disk access"
682
683 def _get_access_id_group(self, target_name):
684 target = self._get_group(target_name)
685 return (target.entity_id, self.const.auth_target_type_group,
686 self.const.auth_grant_group)
687 def _validate_access_group(self, opset, attr):
688 # TODO: check if the opset is relevant for a group
689 if attr is not None:
690 raise CerebrumError, "Can't specify attribute for group access"
691
692 # These three should *really* not be here, but due to this being the
693 # place that "access grant" & friends are defined, this is where
694 # the dns-derived functions need to be too
695 def _get_access_id_dns(self, target):
696 sub = Subnet(self.db)
697 sub.find(target.split('/')[0])
698 return (sub.entity_id,
699 self.const.auth_target_type_dns,
700 self.const.auth_grant_dns)
701 def _validate_access_dns(self, opset, attr):
702 # TODO: check if the opset is relevant for a dns-target
703 if attr is not None:
704 raise CerebrumError("Can't specify attribute for dns access")
705
706 def _get_access_id_global_dns(self, target_name):
707 if target_name:
708 raise CerebrumError("You can't specify an address")
709 return None, self.const.auth_target_type_global_dns, None
710 def _validate_access_global_dns(self, opset, attr):
711 if attr:
712 raise CerebrumError("You can't specify a pattern with global_dns.")
713
714 # access dns <dns-target>
715 all_commands['access_dns'] = Command(
716 ('access', 'dns'), SimpleString(),
717 fs=FormatSuggestion("%-16s %-9s %-9s %s",
718 ("opset", "type", "level", "name"),
719 hdr="%-16s %-9s %-9s %s" %
720 ("Operation set", "Type", "Level", "Name")))
721 def access_dns(self, operator, dns_target):
722 ret = []
723 if '/' in dns_target:
724 # Asking for rights on subnet; IP not of interest
725 for accessor in self._list_access("dns", dns_target,
726 empty_result=[]):
727 accessor["level"] = "Subnet"
728 ret.append(accessor)
729 else:
730 # Asking for rights on IP; need to provide info about
731 # rights on the IP's subnet too
732 for accessor in self._list_access("dns", dns_target + '/',
733 empty_result=[]):
734 accessor["level"] = "Subnet"
735 ret.append(accessor)
736 for accessor in self._list_access("dns", dns_target,
737 empty_result=[]):
738 accessor["level"] = "IP"
739 ret.append(accessor)
740 return ret
741
742
743 def _get_access_id_global_group(self, group):
744 if group is not None and group != "":
745 raise CerebrumError, "Cannot set domain for global access"
746 return None, self.const.auth_target_type_global_group, None
747 def _validate_access_global_group(self, opset, attr):
748 if attr is not None:
749 raise CerebrumError, "Can't specify attribute for global group"
750
751 def _get_access_id_host(self, target_name):
752 target = self._get_host(target_name)
753 return (target.entity_id, self.const.auth_target_type_host,
754 self.const.auth_grant_host)
755 def _validate_access_host(self, opset, attr):
756 if attr is not None:
757 if attr.count('/'):
758 raise CerebrumError, ("The disk pattern should only contain "+
759 "the last component of the path.")
760 try:
761 re.compile(attr)
762 except re.error, e:
763 raise CerebrumError, ("Syntax error in regexp: %s" % e)
764
765 def _get_access_id_global_host(self, target_name):
766 if target_name is not None and target_name != "":
767 raise CerebrumError, ("You can't specify a hostname")
768 return None, self.const.auth_target_type_global_host, None
769 def _validate_access_global_host(self, opset, attr):
770 if attr is not None:
771 raise CerebrumError, ("You can't specify a pattern with "
772 "global_host.")
773
774 def _get_access_id_maildom(self, dom):
775 ed = self._get_email_domain(dom)
776 return (ed.entity_id, self.const.auth_target_type_maildomain,
777 self.const.auth_grant_maildomain)
778 def _validate_access_maildom(self, opset, attr):
779 if attr is not None:
780 raise CerebrumError, ("No attribute with maildom.")
781
782 def _get_access_id_global_maildom(self, dom):
783 if dom is not None and dom != '':
784 raise CerebrumError, "Cannot set domain for global access"
785 return None, self.const.auth_target_type_global_maildomain, None
786 def _validate_access_global_maildom(self, opset, attr):
787 if attr is not None:
788 raise CerebrumError, ("No attribute with global maildom.")
789
790 def _get_access_id_ou(self, ou):
791 ou = self._get_ou(stedkode=ou)
792 return (ou.entity_id, self.const.auth_target_type_ou,
793 self.const.auth_grant_ou)
794 def _validate_access_ou(self, opset, attr):
795 if attr is not None:
796 try:
797 int(self.const.PersonAffiliation(attr))
798 except Errors.NotFoundError:
799 raise CerebrumError, "Unknown affiliation '%s'" % attr
800
801 def _get_access_id_global_ou(self, ou):
802 if ou is not None and ou != '':
803 raise CerebrumError, "Cannot set OU for global access"
804 return None, self.const.auth_target_type_global_ou, None
805 def _validate_access_global_ou(self, opset, attr):
806 if not attr:
807 # This is a policy decision, and should probably be
808 # elsewhere.
809 raise CerebrumError, "Must specify affiliation for global ou access"
810 try:
811 int(self.const.PersonAffiliation(attr))
812 except Errors.NotFoundError:
813 raise CerebrumError("Unknown affiliation: %s" % attr)
814
815 # access list_opsets
816 all_commands['access_list_opsets'] = Command(
817 ('access', 'list_opsets'),
818 fs=FormatSuggestion("%s", ("opset",),
819 hdr="Operation set"))
820 def access_list_opsets(self, operator):
821 baos = BofhdAuthOpSet(self.db)
822 ret = []
823 for r in baos.list():
824 ret.append({'opset': r['name']})
825 ret.sort(lambda x, y: cmp(x['opset'].lower(), y['opset'].lower()))
826 return ret
827
828
829 # access list_alterable [group/maildom/host/disk] [username]
830 hidden_commands['access_list_alterable'] = Command(
831 ('access', 'list_alterable'),
832 SimpleString(optional=True),
833 AccountName(optional=True),
834 fs=FormatSuggestion("%10d %15s %s",
835 ("entity_id", "entity_type", "entity_name")))
836 def access_list_alterable(self, operator, target_type='group',
837 access_holder=None):
838 """List entities that access_holder can moderate."""
839
840 if access_holder is None:
841 account_id = operator.get_entity_id()
842 else:
843 account = self._get_account(access_holder, actype="PosixUser")
844 account_id = account.entity_id
845
846 if not (account_id == operator.get_entity_id() or
847 self.ba.is_superuser(operator.get_entity_id())):
848 raise PermissionDenied("You do not have permission for this operation")
849
850 result = list()
851 matches = self.ba.list_alterable_entities(account_id, target_type)
852 if len(matches) > cereconf.BOFHD_MAX_MATCHES_ACCESS:
853 raise CerebrumError("More than %d (%d) matches. Refusing to return "
854 "result" %
855 (cereconf.BOFHD_MAX_MATCHES_ACCESS, len(matches)))
856 for row in matches:
857 try:
858 entity = self._get_entity(ident=row["entity_id"])
859 except Errors.NotFoundError:
860 self.logger.warn(
861 "Non-existent entity (%s) referenced from auth_op_target",
862 row["entity_id"])
863 continue
864 etype = str(self.const.EntityType(entity.entity_type))
865 ename = self._get_entity_name(entity.entity_id, entity.entity_type)
866 tmp = {"entity_id": row["entity_id"],
867 "entity_type": etype,
868 "entity_name": ename,}
869 if entity.entity_type == self.const.entity_group:
870 tmp["description"] = entity.description
871
872 result.append(tmp)
873 return result
874 # end access_list_alterable
875
876
877 # access show_opset <opset name>
878 all_commands['access_show_opset'] = Command(
879 ('access', 'show_opset'),
880 OpSet(),
881 fs=FormatSuggestion("%-16s %-16s %s",
882 ("op", "attr", "desc"),
883 hdr="%-16s %-16s %s" %
884 ("Operation", "Attribute", "Description")))
885 def access_show_opset(self, operator, opset=None):
886 baos = BofhdAuthOpSet(self.db)
887 try:
888 baos.find_by_name(opset)
889 except Errors.NotFoundError:
890 raise CerebrumError, "Unknown operation set: '%s'" % opset
891 ret = []
892 for r in baos.list_operations():
893 entry = {'op': str(self.const.AuthRoleOp(r['op_code'])),
894 'desc': self.const.AuthRoleOp(r['op_code']).description}
895 attrs = []
896 for r2 in baos.list_operation_attrs(r['op_id']):
897 attrs += [r2['attr']]
898 if not attrs:
899 attrs = [""]
900 for a in attrs:
901 entry_with_attr = entry.copy()
902 entry_with_attr['attr'] = a
903 ret += [entry_with_attr]
904 ret.sort(lambda x,y: cmp(x['op'], y['op']) or cmp(x['attr'], y['attr']))
905 return ret
906
907 # TODO
908 #
909 # To be able to manipulate all aspects of bofhd authentication, we
910 # need a few more commands:
911 #
912 # access create_opset <opset name>
913 # access create_op <opname> <desc>
914 # access delete_op <opname>
915 # access add_to_opset <opset> <op> [<attr>]
916 # access remove_from_opset <opset> <op> [<attr>]
917 #
918 # The opset could be implicitly deleted after the last op was
919 # removed from it.
920
921 # access list operator
922 all_commands['access_list'] = Command(
923 ('access', 'list'),
924 SimpleString(help_ref='id:target:group'),
925 SimpleString(help_ref='string_perm_target_type', optional=True),
926 fs=FormatSuggestion("%-14s %-16s %-30s %-7s",
927 ("opset", "target_type", "target", "attr"),
928 hdr="%-14s %-16s %-30s %-7s" %
929 ("Operation set", "Target type", "Target",
930 "Attr")))
931 def access_list(self, operator, owner, target_type=None):
932 ar = BofhdAuthRole(self.db)
933 aot = BofhdAuthOpTarget(self.db)
934 aos = BofhdAuthOpSet(self.db)
935 owner_id = self.util.get_target(owner, default_lookup="group",
936 restrict_to=[]).entity_id
937 ret = []
938 for role in ar.list(owner_id):
939 aos.clear()
940 aos.find(role['op_set_id'])
941 for r in aot.list(target_id=role['op_target_id']):
942 if target_type is not None and r['target_type'] != target_type:
943 continue
944 if r['entity_id'] is None:
945 target_name = "N/A"
946 elif r['target_type'] == self.const.auth_target_type_maildomain:
947 # FIXME: EmailDomain is not an Entity.
948 ed = Email.EmailDomain(self.db)
949 try:
950 ed.find(r['entity_id'])
951 except (Errors.NotFoundError, ValueError):
952 self.logger.warn("Non-existing entity (e-mail domain) in "
953 "auth_op_target %s:%d" %
954 (r['target_type'], r['entity_id']))
955 continue
956 target_name = ed.email_domain_name
957 elif r['target_type'] == self.const.auth_target_type_ou:
958 ou = self.OU_class(self.db)
959 try:
960 ou.find(r['entity_id'])
961 except (Errors.NotFoundError, ValueError):
962 self.logger.warn("Non-existing entity (ou) in "
963 "auth_op_target %s:%d" %
964 (r['target_type'], r['entity_id']))
965 continue
966 target_name = "%02d%02d%02d (%s)" % (ou.fakultet,
967 ou.institutt,
968 ou.avdeling,
969 ou.short_name)
970 elif r['target_type'] == self.const.auth_target_type_dns:
971 s = Subnet(self.db)
972 # TODO: should Subnet.find() support ints as input?
973 try:
974 s.find('entity_id:%s' % r['entity_id'])
975 except (Errors.NotFoundError, ValueError):
976 self.logger.warn("Non-existing entity (subnet) in "
977 "auth_op_target %s:%d" %
978 (r['target_type'], r['entity_id']))
979 continue
980 target_name = "%s/%s" % (s.subnet_ip, s.subnet_mask)
981 else:
982 try:
983 ety = self._get_entity(ident=r['entity_id'])
984 target_name = self._get_name_from_object(ety)
985 except (Errors.NotFoundError, ValueError):
986 self.logger.warn("Non-existing entity in "
987 "auth_op_target %s:%d" %
988 (r['target_type'], r['entity_id']))
989 continue
990 ret.append({'opset': aos.name,
991 'target_type': r['target_type'],
992 'target': target_name,
993 'attr': r['attr'] or ""})
994 ret.sort(lambda a,b: (cmp(a['target_type'], b['target_type']) or
995 cmp(a['target'], b['target'])))
996 return ret
997
998 def _get_auth_op_target(self, entity_id, target_type, attr=None,
999 any_attr=False, create=False):
1000
1001 """Return auth_op_target(s) associated with (entity_id,
1002 target_type, attr). If any_attr is false, return one
1003 op_target_id or None. If any_attr is true, return list of
1004 matching db_row objects. If create is true, create a new
1005 op_target if no matching row is found."""
1006
1007 if any_attr:
1008 op_targets = []
1009 assert attr is None and create is False
1010 else:
1011 op_targets = None
1012
1013 aot = BofhdAuthOpTarget(self.db)
1014 for r in aot.list(entity_id=entity_id, target_type=target_type,
1015 attr=attr):
1016 if attr is None and not any_attr and r['attr']:
1017 continue
1018 if any_attr:
1019 op_targets.append(r)
1020 else:
1021 # There may be more than one matching op_target, but
1022 # we don't care which one we use -- we will make sure
1023 # not to make duplicates ourselves.
1024 op_targets = int(r['op_target_id'])
1025 if op_targets or not create:
1026 return op_targets
1027 # No op_target found, make a new one.
1028 aot.populate(entity_id, target_type, attr)
1029 aot.write_db()
1030 return aot.op_target_id
1031
1032 def _grant_auth(self, entity_id, opset, target_id, target_type, attr,
1033 entity_name, target_name):
1034 op_target_id = self._get_auth_op_target(target_id, target_type, attr,
1035 create=True)
1036 ar = BofhdAuthRole(self.db)
1037 rows = ar.list(entity_id, opset.op_set_id, op_target_id)
1038 if len(rows) == 0:
1039 ar.grant_auth(entity_id, opset.op_set_id, op_target_id)
1040 return "OK, granted %s access %s to %s %s" % (entity_name,
1041 opset.name,
1042 target_type,
1043 target_name)
1044 return "%s already has %s access to %s %s" % (entity_name,
1045 opset.name,
1046 target_type,
1047 target_name)
1048
1049 def _revoke_auth(self, entity_id, opset, target_id, target_type, attr,
1050 entity_name, target_name):
1051 op_target_id = self._get_auth_op_target(target_id, target_type, attr)
1052 if not op_target_id:
1053 raise CerebrumError, ("No one has matching access to %s" %
1054 target_name)
1055 ar = BofhdAuthRole(self.db)
1056 rows = ar.list(entity_id, opset.op_set_id, op_target_id)
1057 if len(rows) == 0:
1058 return "%s doesn't have %s access to %s %s" % (entity_name,
1059 opset.name,
1060 target_type,
1061 target_name)
1062 ar.revoke_auth(entity_id, opset.op_set_id, op_target_id)
1063 # See if the op_target has any references left, delete it if not.
1064 rows = ar.list(op_target_id=op_target_id)
1065 if len(rows) == 0:
1066 aot = BofhdAuthOpTarget(self.db)
1067 aot.find(op_target_id)
1068 aot.delete()
1069 return "OK, revoked %s access for %s from %s %s" % (opset.name,
1070 entity_name,
1071 target_type,
1072 target_name)
1073
1074 #
1075 # email commands
1076 #
1077
1078 # email add_address <address or account> <address>+
1079 # exchange-relatert-jazz
1080 # made it possible to use this cmd for adding addresses
1081 # to dist group targets
1082 all_commands['email_add_address'] = Command(
1083 ('email', 'add_address'),
1084 SimpleString(help_ref="dlgroup_or_account_name"),
1085 EmailAddress(help_ref='email_address', repeat=True),
1086 perm_filter='can_email_address_add')
1087 def email_add_address(self, operator, name, address):
1088 try:
1089 et, acc = self._get_email_target_and_account(name)
1090 except CerebrumError, e:
1091 # check if a distribution-group with an appropriate target
1092 # is registered by this name
1093 try:
1094 et, grp = self._get_email_target_and_dlgroup(name)
1095 except CerebrumError, e:
1096 raise e
1097 ttype = et.email_target_type
1098 if et.email_target_type == self.const.email_target_deleted:
1099 raise CerebrumError, "Can't add e-mail address to deleted target"
1100 ea = Email.EmailAddress(self.db)
1101 lp, dom = self._split_email_address(address)
1102 ed = self._get_email_domain(dom)
1103 # TODO: change can_email_address_add so that both accounts and
1104 # distribution groups are checked when asserting priviledges
1105 # however, being "postmaster" trumps this, so assertion will be
1106 # correct
1107 self.ba.can_email_address_add(operator.get_entity_id(),
1108 account=acc, domain=ed) or \
1109 self.ba.is_postmaster(operator.get_entity_id())
1110 ea.clear()
1111 try:
1112 ea.find_by_address(address)
1113 raise CerebrumError, "Address already exists (%s)" % address
1114 except Errors.NotFoundError:
1115 pass
1116 ea.clear()
1117 ea.populate(lp, ed.entity_id, et.entity_id)
1118 ea.write_db()
1119 return "OK, added '%s' as email-address for '%s'" % (address, name)
1120
1121 # email remove_address <account> <address>+
1122 # exchange-relatert-jazz
1123 # made it possible to use this cmd for removing addresses
1124 # for dist group targets
1125 all_commands['email_remove_address'] = Command(
1126 ('email', 'remove_address'),
1127 SimpleString(help_ref="dlgroup_or_account_name"),
1128 EmailAddress(repeat=True),
1129 perm_filter='can_email_address_delete')
1130 def email_remove_address(self, operator, name, address):
1131 try:
1132 et, acc = self._get_email_target_and_account(name)
1133 except CerebrumError, e:
1134 # check if a distribution-group with an appropriate target
1135 # is registered by this name
1136 try:
1137 et, grp = self._get_email_target_and_dlgroup(name)
1138 except CerebrumError, e:
1139 raise e
1140 lp, dom = self._split_email_address(address, with_checks=False)
1141 ed = self._get_email_domain(dom)
1142 self.ba.can_email_address_delete(operator.get_entity_id(),
1143 account=acc, domain=ed) or \
1144 self.ba.is_postmaster(operator.get_entity_id())
1145 return self._remove_email_address(et, address)
1146
1147 def _remove_email_address(self, et, address):
1148 ea = Email.EmailAddress(self.db)
1149 try:
1150 ea.find_by_address(address)
1151 except Errors.NotFoundError:
1152 raise CerebrumError, "No such e-mail address <%s>" % address
1153 if ea.get_target_id() != et.entity_id:
1154 raise CerebrumError, ("<%s> is not associated with that target" %
1155 address)
1156 addresses = et.get_addresses()
1157 epat = Email.EmailPrimaryAddressTarget(self.db)
1158 try:
1159 epat.find(et.entity_id)
1160 primary = epat.email_primaddr_id
1161 except Errors.NotFoundError:
1162 primary = None
1163 if primary == ea.entity_id:
1164 if len(addresses) == 1:
1165 # We're down to the last address, remove the primary
1166 epat.delete()
1167 else:
1168 raise CerebrumError, \
1169 "Can't remove primary address <%s>" % address
1170 ea.delete()
1171 if len(addresses) > 1:
1172 # there is at least one address left
1173 return "OK, removed '%s'" % address
1174 # clean up and remove the target.
1175 et.delete()
1176 return "OK, also deleted e-mail target"
1177
1178
1179 # email reassign_address <address> <destination>
1180 all_commands['email_reassign_address'] = Command(
1181 ('email', 'reassign_address'),
1182 EmailAddress(help_ref='email_address'),
1183 AccountName(help_ref='account_name'),
1184 perm_filter='can_email_address_reassign')
1185 def email_reassign_address(self, operator, address, dest):
1186 source_et, source_acc = self._get_email_target_and_account(address)
1187 ttype = source_et.email_target_type
1188 if ttype not in (self.const.email_target_account,
1189 self.const.email_target_deleted):
1190 raise CerebrumError, ("Can't reassign e-mail address from target "+
1191 "type %s") % self.const.EmailTarget(ttype)
1192 dest_acc = self._get_account(dest)
1193 if dest_acc.is_deleted():
1194 raise CerebrumError, ("Can't reassign e-mail address to deleted "+
1195 "account %s") % dest
1196 dest_et = Email.EmailTarget(self.db)
1197 try:
1198 dest_et.find_by_target_entity(dest_acc.entity_id)
1199 except Errors.NotFoundError:
1200 raise CerebrumError, "Account %s has no e-mail target" % dest
1201 if dest_et.email_target_type != self.const.email_target_account:
1202 raise CerebrumError, ("Can't reassign e-mail address to target "+
1203 "type %s") % self.const.EmailTarget(ttype)
1204 if source_et.entity_id == dest_et.entity_id:
1205 return "%s is already connected to %s" % (address, dest)
1206 if (source_acc.owner_type != dest_acc.owner_type or
1207 source_acc.owner_id != dest_acc.owner_id):
1208 raise CerebrumError, ("Can't reassign e-mail address to a "+
1209 "different person.")
1210
1211 self.ba.can_email_address_reassign(operator.get_entity_id(),
1212 dest_acc)
1213
1214 source_epat = Email.EmailPrimaryAddressTarget(self.db)
1215 try:
1216 source_epat.find(source_et.entity_id)
1217 source_epat.delete()
1218 except Errors.NotFoundError:
1219 pass
1220
1221 ea = Email.EmailAddress(self.db)
1222 ea.find_by_address(address)
1223 ea.email_addr_target_id = dest_et.entity_id
1224 ea.write_db()
1225
1226 dest_acc.update_email_addresses()
1227
1228 if (len(source_et.get_addresses()) == 0 and
1229 ttype == self.const.email_target_deleted):
1230 source_et.delete()
1231 return "OK, also deleted e-mail target"
1232
1233 source_acc.update_email_addresses()
1234 return "OK, reassigned %s" % address
1235
1236 all_commands['email_local_delivery'] = Command(
1237 ('email', 'local_delivery'),
1238 AccountName(help_ref='account_name'),
1239 SimpleString(help_ref='string_email_on_off'),
1240 perm_filter='can_email_forward_toggle')
1241
1242 def email_local_delivery(self, operator, uname, on_off):
1243 """Turn on or off local delivery of E-mail."""
1244 acc = self._get_account(uname)
1245 self.ba.can_email_forward_toggle(operator.get_entity_id(), acc)
1246 fw = Email.EmailForward(self.db)
1247 fw.find_by_target_entity(acc.entity_id)
1248 on_off = on_off.lower()
1249 if on_off == 'on':
1250 fw.enable_local_delivery()
1251 elif on_off == 'off':
1252 fw.disable_local_delivery()
1253 else:
1254 raise CerebrumError("Must specify 'on' or 'off'")
1255 return "OK, local delivery turned %s" % on_off
1256
1257 all_commands['email_forward'] = Command(
1258 ('email', 'forward'),
1259 AccountName(),
1260 EmailAddress(),
1261 SimpleString(help_ref='string_email_on_off'),
1262 perm_filer='can_email_forward_toggle')
1263
1264 def email_forward(self, operator, uname, addr, on_off):
1265 """Toggle if a forward is active or not."""
1266 acc = self._get_account(uname)
1267 self.ba.can_email_forward_toggle(operator.get_entity_id(), acc)
1268 fw = Email.EmailForward(self.db)
1269 fw.find_by_target_entity(acc.entity_id)
1270
1271 if addr not in [r['forward_to'] for r in fw.get_forward()]:
1272 raise CerebrumError("Forward address not registered in target")
1273
1274 on_off = on_off.lower()
1275 if on_off == 'on':
1276 fw.enable_forward(addr)
1277 elif on_off == 'off':
1278 fw.disable_forward(addr)
1279 else:
1280 raise CerebrumError("Must specify 'on' or 'off'")
1281 fw.write_db()
1282 return "OK, forward to %s turned %s" % (addr, on_off)
1283
1284 # email add_forward <account>+ <address>+
1285 # account can also be an e-mail address for pure forwardtargets
1286 all_commands['email_add_forward'] = Command(
1287 ('email', 'add_forward'),
1288 AccountName(help_ref='account_name', repeat=True),
1289 EmailAddress(help_ref='email_address', repeat=True),
1290 perm_filter='can_email_forward_edit')
1291
1292 def email_add_forward(self, operator, uname, address):
1293 """Add an email-forward to a email-target asociated with an account."""
1294 et, acc = self._get_email_target_and_account(uname)
1295 if uname.count('@') and not acc:
1296 lp, dom = uname.split('@')
1297 ed = Email.EmailDomain(self.db)
1298 ed.find_by_domain(dom)
1299 self.ba.can_email_forward_edit(operator.get_entity_id(),
1300 domain=ed)
1301 else:
1302 self.ba.can_email_forward_edit(operator.get_entity_id(), acc)
1303 fw = Email.EmailForward(self.db)
1304 fw.find(et.entity_id)
1305 if address == 'local':
1306 fw.enable_local_delivery()
1307 return 'OK, local delivery turned on'
1308 addr = self._check_email_address(address)
1309 if self._forward_exists(fw, addr):
1310 raise CerebrumError("Forward address added already (%s)" % addr)
1311
1312 if fw.get_forward():
1313 raise CerebrumError("Only one forward allowed at a time")
1314
1315 fw.add_forward(addr)
1316 return "OK, added '%s' as forward-address for '%s'" % (
1317 address, uname)
1318
1319 # email delete_forward address
1320 all_commands['email_delete_forward_target'] = Command(
1321 ("email", "delete_forward_target"),
1322 EmailAddress(help_ref='email_address'),
1323 fs=FormatSuggestion([("Deleted forward address: %s", ("address", ))]),
1324 perm_filter='can_email_forward_create')
1325 def email_delete_forward_target(self, operator, address):
1326 """Delete a forward target with associated aliases. Requires primary
1327 address."""
1328
1329 # Allow us to delete an address, even if it is malformed.
1330 lp, dom = self._split_email_address(address, with_checks=False)
1331 ed = self._get_email_domain(dom)
1332 et, acc = self._get_email_target_and_account(address)
1333 self.ba.can_email_forward_edit(operator.get_entity_id(), domain=ed)
1334 epat = Email.EmailPrimaryAddressTarget(self.db)
1335 try:
1336 epat.find(et.entity_id)
1337 # but if one exists, we require the user to supply that
1338 # address, not an arbitrary alias.
1339 if address != self._get_address(epat):
1340 raise CerebrumError("%s is not the primary address of the target" % address)
1341 epat.delete()
1342 except Errors.NotFoundError:
1343 # a forward address does not need a primary address
1344 pass
1345
1346 fw = Email.EmailForward(self.db)
1347 try:
1348 fw.find(et.entity_id)
1349 for f in fw.get_forward():
1350 fw.delete_forward(f['forward_to'])
1351 except Errors.NotFoundError:
1352 # There are som stale forward targets without any address to
1353 # forward to, hence ignore.
1354 pass
1355
1356 result = []
1357 ea = Email.EmailAddress(self.db)
1358 for r in et.get_addresses():
1359 ea.clear()
1360 ea.find(r['address_id'])
1361 result.append({'address': self._get_address(ea)})
1362 ea.delete()
1363 et.delete()
1364 return result
1365
1366 # email remove_forward <account>+ <address>+
1367 # account can also be an e-mail address for pure forwardtargets
1368 all_commands['email_remove_forward'] = Command(
1369 ("email", "remove_forward"),
1370 AccountName(help_ref="account_name", repeat=True),
1371 EmailAddress(help_ref='email_address', repeat=True),
1372 perm_filter='can_email_forward_edit')
1373 def email_remove_forward(self, operator, uname, address):
1374 et, acc = self._get_email_target_and_account(uname)
1375 if uname.count('@') and not acc:
1376 lp, dom = uname.split('@')
1377 ed = Email.EmailDomain(self.db)
1378 ed.find_by_domain(dom)
1379 self.ba.can_email_forward_edit(operator.get_entity_id(),
1380 domain=ed)
1381 else:
1382 self.ba.can_email_forward_edit(operator.get_entity_id(), acc)
1383 fw = Email.EmailForward(self.db)
1384 fw.find(et.entity_id)
1385 if address == 'local':
1386 fw.disable_local_delivery()
1387 return 'OK, local delivery turned off'
1388 addr = self._check_email_address(address)
1389 removed = 0
1390 for a in [addr]:
1391 if self._forward_exists(fw, a):
1392 fw.delete_forward(a)
1393 removed += 1
1394 if not removed:
1395 raise CerebrumError, "No such forward address (%s)" % addr
1396 return "OK, removed '%s'" % address
1397
1398 def _check_email_address(self, address):
1399 """ Check email address syntax.
1400
1401 Accepted syntax:
1402 - 'local'
1403 - <localpart>@<domain>
1404 localpart cannot contain @ or whitespace
1405 domain cannot contain @ or whitespace
1406 domain must have at least one '.'
1407 - Any string where a substring wrapped in <> brackets matches the
1408 above rule.
1409 - Valid examples: jdoe@example.com
1410 <jdoe>@<example.com>
1411 Jane Doe <jdoe@example.com>
1412
1413 NOTE: Raises CerebrumError if address is invalid
1414
1415 @rtype: str
1416 @return: address.strip()
1417
1418 """
1419 address = address.strip()
1420 if address.find("@") == -1:
1421 raise CerebrumError, "E-mail addresses must include the domain name"
1422
1423 error_msg = ("Invalid e-mail address: %s\n"
1424 "Valid input:\n"
1425 "jdoe@example.com\n"
1426 "<jdoe>@<example.com>\n"
1427 "Jane Doe <jdoe@example.com>" % address)
1428 # Check if we either have a string consisting only of an address,
1429 # or if we have an bracketed address prefixed by a name. At last,
1430 # verify that the email is RFC-compliant.
1431 if not ((re.match(r'[^@\s]+@[^@\s.]+\.[^@\s]+$', address) or
1432 re.search(r'<[^@>\s]+@[^@>\s.]+\.[^@>\s]+>$', address))):
1433 raise CerebrumError(error_msg)
1434
1435 # Strip out angle brackets before running proper validation, as the
1436 # flanker address parser gets upset if domain is wrapped in them.
1437 val_adr = address.replace('<', '').replace('>', '')
1438 if not email_validator.parse(val_adr):
1439 raise CerebrumError(error_msg)
1440 return address
1441
1442 def _forward_exists(self, fw, addr):
1443 for r in fw.get_forward():
1444 if r['forward_to'] == addr:
1445 return True
1446 return False
1447
1448 # email forward_info
1449 all_commands['email_forward_info'] = Command(
1450 ('email', 'forward_info'),
1451 EmailAddress(),
1452 perm_filter='can_email_forward_info',
1453 fs=FormatSuggestion([
1454 ('%s', ('id', ))]))
1455
1456 def email_forward_info(self, operator, forward_to):
1457 """List owners of email forwards."""
1458 self.ba.can_email_forward_info(operator.get_entity_id())
1459 ef = Email.EmailForward(self.db)
1460 et = Email.EmailTarget(self.db)
1461 ac = Utils.Factory.get('Account')(self.db)
1462 ret = []
1463
1464 # Different output format for different input.
1465 rfun = lambda r: (r if '%' not in forward_to else
1466 '%-12s %s' % (r, fwd['forward_to']))
1467
1468 for fwd in ef.search(forward_to):
1469 try:
1470 et.clear()
1471 et.find(fwd['target_id'])
1472 ac.clear()
1473 ac.find(et.email_target_entity_id)
1474 ret.append({'id': rfun(ac.account_name)})
1475 except Errors.NotFoundError:
1476 ret.append({'id': rfun('id:%s' % et.entity_id)})
1477 return ret
1478
1479 # email info <account>+
1480 all_commands['email_info'] = Command(
1481 ("email", "info"),
1482 # AccountName(help_ref="account_name", repeat=True),
1483 SimpleString(help_ref="dlgroup_or_account_name", repeat=True),
1484 perm_filter='can_email_info',
1485 fs=FormatSuggestion([
1486 ("Type: %s", ("target_type",)),
1487 ("History: entity history id:%d", ("target_id",)),
1488 #
1489 # target_type == Account
1490 #
1491 ("Account: %s\nMail server: %s (%s)",
1492 ("account", "server", "server_type")),
1493 ("Primary address: %s",
1494 ("def_addr", )),
1495 ("Alias value: %s",
1496 ("alias_value", )),
1497 # We use valid_addr_1 and (multiple) valid_addr to enable
1498 # programs to get the information reasonably easily, while
1499 # still keeping the suggested output format pretty.
1500 ("Valid addresses: %s",
1501 ("valid_addr_1", )),
1502 (" %s",
1503 ("valid_addr",)),
1504 ("Mail quota: %d MiB, warn at %d%% (not enforced)",
1505 ("dis_quota_hard", "dis_quota_soft")),
1506 ("Mail quota: %d MiB, warn at %d%% (%s used (MiB))",
1507 ("quota_hard", "quota_soft", "quota_used")),
1508 (" (currently %d MiB on server)",
1509 ("quota_server",)),
1510 ("HomeMDB: %s",
1511 ("homemdb", )),
1512 # TODO: change format so that ON/OFF is passed as separate value.
1513 # this must be coordinated with webmail code.
1514 ("Forwarding: %s",
1515 ("forward_1", )),
1516 (" %s",
1517 ("forward", )),
1518 # exchange-relatert-jazz
1519 #
1520 # target_type == dlgroup
1521 #
1522 ("Dl group: %s",
1523 ("name", )),
1524 ("Group id: %d",
1525 ("group_id", )),
1526 ("Display name: %s",
1527 ("displayname", )),
1528 ("Primary address: %s",
1529 ("primary", )),
1530 # We use valid_addr_1 and (multiple) valid_addr to enable
1531 # programs to get the information reasonably easily, while
1532 # still keeping the suggested output format pretty.
1533 #("Valid addresses: %s",
1534 #("valid_addr_1", )),
1535 #(" %s",
1536 # ("valid_addr",)),
1537 ("Valid addresses: %s",
1538 ("aliases", )),
1539 ("Hidden addr list: %s",
1540 ('hidden', )),
1541 #
1542 # target_type == Sympa
1543 #
1544 ("Mailing list: %s",
1545 ("sympa_list",)),
1546 ("Alias: %s",
1547 ("sympa_alias_1",)),
1548 (" %s",
1549 ("sympa_alias",)),
1550 ("Request: %s",
1551 ("sympa_request_1",)),
1552 (" %s",
1553 ("sympa_request",)),
1554 ("Owner: %s",
1555 ("sympa_owner_1",)),
1556 (" %s",
1557 ("sympa_owner",)),
1558 ("Editor: %s",
1559 ("sympa_editor_1",)),
1560 (" %s",
1561 ("sympa_editor",)),
1562 ("Subscribe: %s",
1563 ("sympa_subscribe_1",)),
1564 (" %s",
1565 ("sympa_subscribe",)),
1566 ("Unsubscribe: %s",
1567 ("sympa_unsubscribe_1",)),
1568 (" %s",
1569 ("sympa_unsubscribe",)),
1570 ("Delivery host: %s",
1571 ("sympa_delivery_host",)),
1572 # target_type == multi
1573 ("Forward to group: %s",
1574 ("multi_forward_gr",)),
1575 ("Expands to: %s",
1576 ("multi_forward_1",)),
1577 (" %s",
1578 ("multi_forward",)),
1579 # target_type == file
1580 ("File: %s\n"+
1581 "Save as: %s",
1582 ("file_name", "file_runas")),
1583 # target_type == pipe
1584 ("Command: %s\n"+
1585 "Run as: %s",
1586 ("pipe_cmd", "pipe_runas")),
1587 # target_type == RT
1588 ("RT queue: %s on %s\n"+
1589 "Action: %s\n"+
1590 "Run as: %s",
1591 ("rt_queue", "rt_host", "rt_action","pipe_runas")),
1592 # target_type == forward
1593 ("Address: %s",
1594 ("fw_target",)),
1595 ("Forwarding: %s (%s)",
1596 ("fw_addr_1", "fw_enable_1")),
1597 (" %s (%s)",
1598 ("fw_addr", "fw_enable")),
1599 #
1600 # both account and Sympa
1601 #
1602 ("Spam level: %s (%s)\nSpam action: %s (%s)",
1603 ("spam_level", "spam_level_desc", "spam_action", "spam_action_desc")),
1604 ("Filters: %s",
1605 ("filters",)),
1606 ("Status: %s",
1607 ("status",)),
1608 ]))
1609 def email_info(self, operator, name):
1610 try:
1611 et, acc = self._get_email_target_and_account(name)
1612 except CerebrumError, e:
1613 # exchange-relatert-jazz
1614 # check if a distribution-group with an appropriate target
1615 # is registered by this name
1616 try:
1617 et, grp = self._get_email_target_and_dlgroup(name)
1618 except CerebrumError, e:
1619 # handle accounts with email address stored in contact_info
1620 try:
1621 ac = self._get_account(name)
1622 return self._email_info_contact_info(operator, ac)
1623 except CerebrumError:
1624 pass
1625 raise e
1626
1627 ttype = et.email_target_type
1628 ttype_name = str(self.const.EmailTarget(ttype))
1629
1630 ret = []
1631
1632 if ttype not in (self.const.email_target_Sympa,
1633 self.const.email_target_pipe,
1634 self.const.email_target_RT,
1635 self.const.email_target_dl_group):
1636 ret += [
1637 {'target_type': ttype_name,
1638 'target_id': et.entity_id, }
1639 ]
1640
1641 epat = Email.EmailPrimaryAddressTarget(self.db)
1642 try:
1643 epat.find(et.entity_id)
1644 except Errors.NotFoundError:
1645 if ttype == self.const.email_target_account:
1646 ret.append({'def_addr': "<none>"})
1647 else:
1648 # exchange-relatert-jazz
1649 # drop def_addr here, it's introduced at proper placing later
1650 if ttype != self.const.email_target_dl_group:
1651 ret.append({'def_addr': self._get_address(epat)})
1652
1653 if ttype not in (self.const.email_target_Sympa,
1654 # exchange-relatert-jazz
1655 # drop fetching valid addrs,
1656 # it's done in a proper place latter
1657 self.const.email_target_dl_group):
1658 # We want to split the valid addresses into multiple
1659 # parts for MLs, so there is special code for that.
1660 addrs = self._get_valid_email_addrs(et, special=True, sort=True)
1661 if not addrs: addrs = ["<none>"]
1662 ret.append({'valid_addr_1': addrs[0]})
1663 for addr in addrs[1:]:
1664 ret.append({"valid_addr": addr})
1665
1666 if ttype == self.const.email_target_Sympa:
1667 ret += self._email_info_sympa(operator, name, et)
1668 elif ttype == self.const.email_target_dl_group:
1669 ret += self._email_info_dlgroup(name)
1670 elif ttype == self.const.email_target_multi:
1671 ret += self._email_info_multi(name, et)
1672 elif ttype == self.const.email_target_file:
1673 ret += self._email_info_file(name, et)
1674 elif ttype == self.const.email_target_pipe:
1675 ret += self._email_info_pipe(name, et)
1676 elif ttype == self.const.email_target_RT:
1677 ret += self._email_info_rt(name, et)
1678 elif ttype == self.const.email_target_forward:
1679 ret += self._email_info_forward(name, et)
1680 elif (ttype == self.const.email_target_account,
1681 # exchange-relatert jazz
1682 # This should be changed, distgroups will have
1683 # target_type=deleted and we will no longer
1684 # be able to assume "deleted" means that
1685 # target_entity_type is account
1686 # <TODO>
1687 ttype == self.const.email_target_deleted):
1688 ret += self._email_info_account(operator, acc, et, addrs)
1689 else:
1690 raise CerebrumError, ("email info for target type %s isn't "
1691 "implemented") % ttype_name
1692
1693 # Only the account owner and postmaster can see account settings, and
1694 # that is handled properly in _email_info_account.
1695 if not ttype in (self.const.email_target_account,
1696 self.const.email_target_deleted):
1697 ret += self._email_info_spam(et)
1698 ret += self._email_info_filters(et)
1699 ret += self._email_info_forwarding(et, name)
1700 return ret
1701
1702 def _email_info_contact_info(self, operator, acc):
1703 """Some accounts doesn't have an e-mail account, but could have stored
1704 an e-mail address in the its contact_info.
1705
1706 Note that this method raises an exception if no such contact_info
1707 address was found."""
1708 addresses = acc.get_contact_info(type=self.const.contact_email)
1709 if not addresses:
1710 raise CerebrumError("No contact info for: %s" % acc.account_name)
1711 ret = [{'target_type': 'entity_contact_info'},]
1712 return ret + [{'valid_addr_1': a['contact_value']} for a in addresses]
1713
1714 def _email_info_account(self, operator, acc, et, addrs):
1715 self.ba.can_email_info(operator.get_entity_id(), acc)
1716 ret = self._email_info_basic(acc, et)
1717 try:
1718 self.ba.can_email_info(operator.get_entity_id(), acc)
1719 except PermissionDenied:
1720 pass
1721 else:
1722 ret += self._email_info_spam(et)
1723 if not et.email_target_type == self.const.email_target_deleted:
1724 # No need to get details for deleted accounts
1725 ret += self._email_info_detail(acc)
1726 ret += self._email_info_forwarding(et, addrs)
1727 ret += self._email_info_filters(et)
1728
1729 # Tell what addresses can be deleted:
1730 ea = Email.EmailAddress(self.db)
1731 dom = Email.EmailDomain(self.db)
1732 domains = acc.get_prospect_maildomains(
1733 use_default_domain=cereconf.EMAIL_DEFAULT_DOMAIN)
1734 for domain in cereconf.EMAIL_NON_DELETABLE_DOMAINS:
1735 dom.clear()
1736 dom.find_by_domain(domain)
1737 domains.append(dom.entity_id)
1738
1739 deletables = []
1740 for addr in et.get_addresses(special=True):
1741 ea.clear()
1742 ea.find(addr['address_id'])
1743 if ea.email_addr_domain_id not in domains:
1744 deletables.append(ea.get_address())
1745 ret.append({'deletable': deletables})
1746 return ret
1747
1748 def _get_valid_email_addrs(self, et, special=False, sort=False):
1749 """Return a list of all valid e-mail addresses for the given
1750 EmailTarget. Keep special domain names intact if special is
1751 True, otherwise re-write them into real domain names."""
1752 addrs = [(r['local_part'], r['domain'])
1753 for r in et.get_addresses(special=special)]
1754 if sort:
1755 addrs.sort(lambda x,y: cmp(x[1], y[1]) or cmp(x[0],y[0]))
1756 return ["%s@%s" % a for a in addrs]
1757
1758 def _email_info_basic(self, acc, et):
1759 info = {}
1760 data = [ info ]
1761 if (et.email_target_type != self.const.email_target_Sympa and
1762 et.email_target_alias is not None):
1763 info['alias_value'] = et.email_target_alias
1764 info["account"] = acc.account_name
1765 if et.email_server_id:
1766 es = Email.EmailServer(self.db)
1767 es.find(et.email_server_id)
1768 info["server"] = es.name
1769 type = int(es.email_server_type)
1770 info["server_type"] = str(self.const.EmailServerType(type))
1771 else:
1772 info["server"] = "<none>"
1773 info["server_type"] = "N/A"
1774 return data
1775
1776 def _email_info_spam(self, target):
1777 info = []
1778 esf = Email.EmailSpamFilter(self.db)
1779 try:
1780 esf.find(target.entity_id)
1781 spam_lev = self.const.EmailSpamLevel(esf.email_spam_level)
1782 spam_act = self.const.EmailSpamAction(esf.email_spam_action)
1783 info.append({'spam_level': str(spam_lev),
1784 'spam_level_desc': spam_lev.description,
1785 'spam_action': str(spam_act),
1786 'spam_action_desc': spam_act.description})
1787 except Errors.NotFoundError:
1788 pass
1789 return info
1790
1791 def _email_info_filters(self, target):
1792 filters = []
1793 info ={}
1794 etf = Email.EmailTargetFilter(self.db)
1795 for f in etf.list_email_target_filter(target_id=target.entity_id):
1796 filters.append(str(Email._EmailTargetFilterCode(f['filter'])))
1797 if len(filters) > 0:
1798 info["filters"] = ", ".join([x for x in filters]),
1799 else:
1800 info["filters"] = "None"
1801 return [ info ]
1802
1803 def _email_info_detail(self, acc):
1804 info = []
1805 eq = Email.EmailQuota(self.db)
1806 try:
1807 eq.find_by_target_entity(acc.entity_id)
1808 et = Email.EmailTarget(self.db)
1809 et.find_by_target_entity(acc.entity_id)
1810 es = Email.EmailServer(self.db)
1811 es.find(et.email_server_id)
1812
1813 # exchange-relatert-jazz
1814 # since Exchange-users will have a different kind of
1815 # server this code will not be affected at Exchange
1816 # roll-out It may, however, be removed as soon as
1817 # migration is completed (up to and including
1818 # "dis_quota_soft': eq.email_quota_soft})")
1819 if es.email_server_type == self.const.email_server_type_cyrus:
1820 pw = self.db._read_password(cereconf.CYRUS_HOST,
1821 cereconf.CYRUS_ADMIN)
1822 used = 'N/A'; limit = None
1823 try:
1824 cyrus = Utils.CerebrumIMAP4_SSL(es.name, ssl_version=ssl.PROTOCOL_TLSv1)
1825 # IVR 2007-08-29 If the server is too busy, we do not want
1826 # to lock the entire bofhd.
1827 # 5 seconds should be enough
1828 cyrus.socket().settimeout(5)
1829 cyrus.login(cereconf.CYRUS_ADMIN, pw)
1830 res, quotas = cyrus.getquota("user." + acc.account_name)
1831 cyrus.socket().settimeout(None)
1832 if res == "OK":
1833 for line in quotas:
1834 try:
1835 folder, qtype, qused, qlimit = line.split()
1836 if qtype == "(STORAGE":
1837 used = str(int(qused)/1024)
1838 limit = int(qlimit.rstrip(")"))/1024
1839 except ValueError:
1840 # line.split fails e.g. because quota isn't set on server
1841 folder, junk = line.split()
1842 self.logger.warning("No IMAP quota set for '%s'" % acc.account_name)
1843 used = "N/A"
1844 limit = None
1845 except (TimeoutException, socket.error):
1846 used = 'DOWN'
1847 except ConnectException, e:
1848 used = str(e)
1849 except imaplib.IMAP4.error, e:
1850 used = 'DOWN'
1851 info.append({'quota_hard': eq.email_quota_hard,
1852 'quota_soft': eq.email_quota_soft,
1853 'quota_used': used})
1854 if limit is not None and limit != eq.email_quota_hard:
1855 info.append({'quota_server': limit})
1856 else:
1857 info.append({'dis_quota_hard': eq.email_quota_hard,
1858 'dis_quota_soft': eq.email_quota_soft})
1859 except Errors.NotFoundError:
1860 pass
1861 # exchange-relatert-jazz
1862 # delivery for exchange-mailboxes is not regulated through
1863 # LDAP, and LDAP should not be checked there my be some need
1864 # to implement support for checking if delivery is paused in
1865 # Exchange, but at this point only very vague explanation has
1866 # been given and priority is therefore low
1867 if acc.has_spread(self.const.spread_uit_exchange):
1868 return info
1869 # Check if the ldapservers have set mailPaused
1870 if self._email_delivery_stopped(acc.account_name):
1871 info.append({'status': 'Paused (migrating to new server)'})
1872
1873 return info
1874
1875 def _email_info_forwarding(self, target, addrs):
1876 info = []
1877 forw = []
1878 ef = Email.EmailForward(self.db)
1879 ef.find(target.entity_id)
1880 for r in ef.get_forward():
1881 enabled = 'on' if (r['enable'] == 'T') else 'off'
1882 forw.append("%s (%s) " % (r['forward_to'], enabled))
1883 # for aesthetic reasons, print "+ local delivery" last
1884 if ef.local_delivery:
1885 forw.append("+ local delivery (on)")
1886 if forw:
1887 info.append({'forward_1': forw[0]})
1888 for idx in range(1, len(forw)):
1889 info.append({'forward': forw[idx]})
1890 return info
1891
1892 def _email_info_dlgroup(self, groupname):
1893 et, dl_group = self._get_email_target_and_dlgroup(groupname)
1894 ret = []
1895 # we need to make the return value conform with the
1896 # client requeirements
1897 tmpret = dl_group.get_distgroup_attributes_and_targetdata()
1898 for x in tmpret:
1899 if tmpret[x] == 'T':
1900 ret.append({x: 'Yes'})
1901 continue
1902 elif tmpret[x] == 'F':
1903 ret.append({x: 'No'})
1904 continue
1905 ret.append({x: tmpret[x]})
1906 return ret
1907
1908 def _email_info_sympa(self, operator, addr, et):
1909 """Collect Sympa-specific information for a ML L{addr}."""
1910
1911 def fish_information(suffix, local_part, domain, listname):
1912 """Generate an entry for sympa info for the specified address.
1913
1914 @type address: basestring
1915 @param address:
1916 Is the address we are looking for (we locate ETs based on the
1917 alias value in _sympa_addr2alias).
1918 @type et: EmailTarget instance
1919
1920 @rtype: sequence (of dicts of basestring to basestring)
1921 @return:
1922 A sequence of dicts suitable for merging into return value from
1923 email_info_sympa.
1924 """
1925
1926 result = []
1927 address = "%(local_part)s-%(suffix)s@%(domain)s" % locals()
1928 target_alias = None
1929 for a, alias in self._sympa_addr2alias:
1930 a = a % locals()
1931 if a == address:
1932 target_alias = alias % locals()
1933 break
1934
1935 # IVR 2008-08-05 TBD Is this an error? All sympa ETs must have an
1936 # alias in email_target.
1937 if target_alias is None:
1938 return result
1939
1940 try:
1941 # Do NOT change et's (parameter's) state.
1942 et_tmp = Email.EmailTarget(self.db)
1943 et_tmp.clear()
1944 et_tmp.find_by_alias(target_alias)
1945 except Errors.NotFoundError:
1946 return result
1947
1948 addrs = et_tmp.get_addresses()
1949 if not addrs:
1950 return result
1951
1952 pattern = '%(local_part)s@%(domain)s'
1953 result.append({'sympa_' + suffix + '_1': pattern % addrs[0]})
1954 for idx in range(1, len(addrs)):
1955 result.append({'sympa_' + suffix: pattern % addrs[idx]})
1956 return result
1957 # end fish_information
1958
1959 # listname may be one of the secondary addresses.
1960 # email info sympatest@domain MUST be equivalent to
1961 # email info sympatest-admin@domain.
1962 listname = self._get_sympa_list(addr)
1963 ret = [{"sympa_list": listname}]
1964 if listname.count('@') == 0:
1965 lp, dom = listname, addr.split('@')[1]
1966 else:
1967 lp, dom = listname.split('@')
1968
1969 ed = Email.EmailDomain(self.db)
1970 ed.find_by_domain(dom)
1971 ea = Email.EmailAddress(self.db)
1972 try:
1973 ea.find_by_local_part_and_domain(lp, ed.entity_id)
1974 except Errors.NotFoundError:
1975 raise CerebrumError, ("Address %s exists, but the list it points "
1976 "to, %s, does not") % (addr, listname)
1977 # now find all e-mail addresses
1978 et_sympa = Email.EmailTarget(self.db)
1979 et_sympa.clear()
1980 et_sympa.find(ea.email_addr_target_id)
1981 addrs = self._get_valid_email_addrs(et_sympa, sort=True)
1982 # IVR 2008-08-21 According to postmasters, only superusers should see
1983 # forwarding and delivery host information
1984 if self.ba.is_postmaster(operator.get_entity_id()):
1985 if et_sympa.email_server_id is None:
1986 delivery_host = "N/A (this is an error)"
1987 else:
1988 delivery_host = self._get_email_server(et_sympa.email_server_id).name
1989 ret.append({"sympa_delivery_host": delivery_host})
1990 ret += self._email_info_forwarding(et_sympa, addrs)
1991 aliases = []
1992 for row in et_sympa.get_addresses():
1993 a = "%(local_part)s@%(domain)s" % row
1994 if a == listname:
1995 continue
1996 aliases.append(a)
1997 if aliases:
1998 ret.append({"sympa_alias_1": aliases[0]})
1999 for next_alias in aliases[1:]:
2000 ret.append({"sympa_alias": next_alias})
2001
2002 for suffix in ("owner", "request", "editor", "subscribe", "unsubscribe"):
2003 ret.extend(fish_information(suffix, lp, dom, listname))
2004 return ret
2005 # end _email_info_sympa
2006
2007
2008 def _email_info_multi(self, addr, et):
2009 ret = []
2010 if et.email_target_entity_type != self.const.entity_group:
2011 ret.append({'multi_forward_gr': 'ENTITY TYPE OF %d UNKNOWN' %
2012 et.email_target_entity_id})
2013 else:
2014 group = self.Group_class(self.db)
2015 acc = self.Account_class(self.db)
2016 try:
2017 group.find(et.email_target_entity_id)
2018 except Errors.NotFoundError:
2019 ret.append({'multi_forward_gr': 'Unknown group %d' %
2020 et.email_target_entity_id})
2021 return ret
2022 ret.append({'multi_forward_gr': group.group_name})
2023
2024 fwds = list()
2025 for row in group.search_members(group_id=group.entity_id,
2026 member_type=self.const.entity_account):
2027 acc.clear()
2028 acc.find(row["member_id"])
2029 try:
2030 addr = acc.get_primary_mailaddress()
2031 except Errors.NotFoundError:
2032 addr = "(account %s has no e-mail)" % acc.account_name
2033 fwds.append(addr)
2034 if fwds:
2035 ret.append({'multi_forward_1': fwds[0]})
2036 for idx in range(1, len(fwds)):
2037 ret.append({'multi_forward': fwds[idx]})
2038 return ret
2039
2040 def _email_info_file(self, addr, et):
2041 account_name = "<not set>"
2042 if et.email_target_using_uid:
2043 acc = self._get_account(et.email_target_using_uid, idtype='id')
2044 account_name = acc.account_name
2045 return [{'file_name': et.get_alias(),
2046 'file_runas': account_name}]
2047
2048 def _email_info_pipe(self, addr, et):
2049 acc = self._get_account(et.email_target_using_uid, idtype='id')
2050 return [{'pipe_cmd': et.get_alias(), 'pipe_runas': acc.account_name}]
2051
2052 def _email_info_rt(self, addr, et):
2053 m = re.match(self._rt_patt, et.get_alias())
2054 acc = self._get_account(et.email_target_using_uid, idtype='id')
2055 return [{'rt_action': m.group(1),
2056 'rt_queue': m.group(2),
2057 'rt_host': m.group(3),
2058 'pipe_runas': acc.account_name}]
2059
2060 def _email_info_forward(self, addr, et):
2061 data = []
2062 # et.email_target_alias isn't used for anything, it's often
2063 # a copy of one of the forward addresses, but that's just a
2064 # waste of bytes, really.
2065 ef = Email.EmailForward(self.db)
2066 try:
2067 ef.find(et.entity_id)
2068 except Errors.NotFoundError:
2069 data.append({'fw_addr_1': '<none>', 'fw_enable': 'off'})
2070 else:
2071 forw = ef.get_forward()
2072 if forw:
2073 data.append({'fw_addr_1': forw[0]['forward_to'],
2074 'fw_enable_1': self._onoff(forw[0]['enable'])})
2075 for idx in range(1, len(forw)):
2076 data.append({'fw_addr': forw[idx]['forward_to'],
2077 'fw_enable': self._onoff(forw[idx]['enable'])})
2078 return data
2079
2080 def _email_delivery_stopped(self, user):
2081 # Delayed import so the script can run on machines without ldap
2082 # module
2083 import ldap, ldap.filter, ldap.ldapobject
2084 ldapconns = [ldap.ldapobject.ReconnectLDAPObject("ldap://%s/" % server)
2085 for server in cereconf.LDAP_SERVERS]
2086 userfilter = ("(&(target=%s)(mailPause=TRUE))" %
2087 ldap.filter.escape_filter_chars(user))
2088 for conn in ldapconns:
2089 try:
2090 # FIXME: cereconf.LDAP_MAIL['dn'] has a bogus value, so we
2091 # must hardcode the DN.
2092 res = conn.search_s("cn=targets,cn=mail,dc=uit,dc=no",
2093 ldap.SCOPE_ONELEVEL, userfilter, ["1.1"])
2094 if len(res) != 1:
2095 return False
2096 except ldap.LDAPError, e:
2097 self.logger.error("LDAP search failed: %s", e)
2098 return False
2099
2100 return True
2101
2102 # email show_reservation_status
2103 all_commands['email_show_reservation_status'] = Command(
2104 ('email', 'show_reservation_status'), AccountName(),
2105 fs=FormatSuggestion(
2106 [("%-9s %s", ("uname", "hide"))]),
2107 perm_filter='is_postmaster')
2108
2109 def email_show_reservation_status(self, operator, uname):
2110 """Display reservation status for a person."""
2111 if not self.ba.is_postmaster(operator.get_entity_id()):
2112 raise PermissionDenied('Access to this command is restricted')
2113 hidden = True
2114 account = self._get_account(uname)
2115 if account.owner_type == self.const.entity_person:
2116 person = self._get_person('entity_id', account.owner_id)
2117 if person.has_e_reservation():
2118 hidden = True
2119 elif person.get_primary_account() != account.entity_id:
2120 hidden = True
2121 else:
2122 hidden = False
2123 return {'uname': uname, 'hide': 'hidden' if hidden else 'visible'}
2124
2125 # email modify_name
2126 all_commands['email_mod_name'] = Command(
2127 ("email", "mod_name"),PersonId(help_ref="person_id_other"),
2128 PersonName(help_ref="person_name_first"),
2129 PersonName(help_ref="person_name_last"),
2130 fs=FormatSuggestion("Name and e-mail address altered for: %i",
2131 ("person_id",)),
2132 perm_filter='can_email_mod_name')
2133 def email_mod_name(self, operator, person_id, firstname, lastname):
2134 person = self._get_person(*self._map_person_id(person_id))
2135 self.ba.can_email_mod_name(operator.get_entity_id(), person=person,
2136 firstname=firstname, lastname=lastname)
2137 source_system = self.const.system_override
2138 person.affect_names(source_system,
2139 self.const.name_first,
2140 self.const.name_last,
2141 self.const.name_full)
2142 if lastname == "":
2143 raise CerebrumError, "A last name is required"
2144 if firstname == "":
2145 fullname = lastname
2146 else:
2147 fullname = firstname + " " + lastname
2148 person.populate_name(self.const.name_first, firstname)
2149 person.populate_name(self.const.name_last, lastname)
2150 person.populate_name(self.const.name_full, fullname)
2151 person._update_cached_names()
2152 try:
2153 person.write_db()
2154 except self.db.DatabaseError, m:
2155 raise CerebrumError, "Database error: %s" % m
2156 return {'person_id': person.entity_id}
2157
2158 # email primary_address <address>
2159 all_commands['email_primary_address'] = Command(
2160 ("email", "primary_address"),
2161 EmailAddress(),
2162 fs=FormatSuggestion([("New primary address: '%s'", ("address", ))]),
2163 perm_filter="is_postmaster")
2164 def email_primary_address(self, operator, addr):
2165 if not self.ba.is_postmaster(operator.get_entity_id()):
2166 raise PermissionDenied("Currently limited to superusers")
2167
2168 et, ea = self._get_email_target_and_address(addr)
2169 if et.email_target_type == self.const.email_target_dl_group:
2170 return "Cannot change primary for distribution group %s" % addr
2171 return self._set_email_primary_address(et, ea, addr)
2172
2173 def _set_email_primary_address(self, et, ea, addr):
2174 epat = Email.EmailPrimaryAddressTarget(self.db)
2175 try:
2176 epat.find(et.entity_id)
2177 except Errors.NotFoundError:
2178 epat.clear()
2179 epat.populate(ea.entity_id, parent=et)
2180 else:
2181 if epat.email_primaddr_id == ea.entity_id:
2182 return "No change: '%s'" % addr
2183 epat.email_primaddr_id = ea.entity_id
2184 epat.write_db()
2185 return {'address': addr}
2186
2187 # email create_pipe <address> <uname> <command>
2188 all_commands['email_create_pipe'] = Command(
2189 ("email", "create_pipe"),
2190 EmailAddress(help_ref="email_address"),
2191 AccountName(),
2192 SimpleString(help_ref="command_line"),
2193 perm_filter="can_email_pipe_create")
2194 def email_create_pipe(self, operator, addr, uname, cmd):
2195 lp, dom = self._split_email_address(addr)
2196 ed = self._get_email_domain(dom)
2197 self.ba.can_email_pipe_create(operator.get_entity_id(), ed)
2198 acc = self._get_account(uname)
2199 ea = Email.EmailAddress(self.db)
2200 try:
2201 ea.find_by_local_part_and_domain(lp, ed.entity_id)
2202 except Errors.NotFoundError:
2203 pass
2204 else:
2205 raise CerebrumError, "%s already exists" % addr
2206 et = Email.EmailTarget(self.db)
2207 if not cmd.startswith('|'):
2208 cmd = '|' + cmd
2209 et.populate(self.const.email_target_pipe, alias=cmd,
2210 using_uid=acc.entity_id)
2211 et.write_db()
2212 ea.clear()
2213 ea.populate(lp, ed.entity_id, et.entity_id)
2214 ea.write_db()
2215 self._register_spam_settings(addr, self.const.email_target_pipe)
2216 self._register_filter_settings(addr, self.const.email_target_pipe)
2217 return "OK, created pipe address %s" % addr
2218
2219 # email delete_pipe <address>
2220 all_commands['email_delete_pipe'] = Command(
2221 ("email", "delete_pipe"),
2222 EmailAddress(help_ref="email_address"),
2223 perm_filter="can_email_pipe_create")
2224 def email_delete_pipe(self, operator, addr):
2225 lp, dom = self._split_email_address(addr, with_checks=False)
2226 ed = self._get_email_domain(dom)
2227 self.ba.can_email_pipe_create(operator.get_entity_id(), ed)
2228 ea = Email.EmailAddress(self.db)
2229 et = Email.EmailTarget(self.db)
2230 try:
2231 ea.clear()
2232 ea.find_by_address(addr)
2233 except Errors.NotFoundError:
2234 raise CerebrumError, "No such address %s" % addr
2235 try:
2236 et.clear()
2237 et.find(ea.email_addr_target_id)
2238 except Errors.NotFoundError:
2239 raise CerebrumError, "No e-mail target for %s" % addr
2240 for a in et.get_addresses():
2241 ea.clear()
2242 ea.find(a['address_id'])
2243 ea.delete()
2244 ea.write_db()
2245 et.delete()
2246 et.write_db()
2247 return "Ok, deleted pipe for address %s" % addr
2248
2249 # email failure_message <username> <message>
2250 all_commands['email_failure_message'] = Command(
2251 ("email", "failure_message"),
2252 AccountName(help_ref="account_name"),
2253 SimpleString(help_ref="email_failure_message"),
2254 perm_filter="can_email_set_failure")
2255 def email_failure_message(self, operator, uname, message):
2256 if not self.ba.is_postmaster(operator.get_entity_id()):
2257 raise PermissionDenied("Currently limited to superusers")
2258 et, acc = self._get_email_target_and_account(uname)
2259 if et.email_target_type != self.const.email_target_deleted:
2260 raise CerebrumError, ("You can only set the failure message "
2261 "for deleted users")
2262 self.ba.can_email_set_failure(operator.get_entity_id(), acc)
2263 if message.strip() == '':
2264 message = None
2265 else:
2266 # It's not ideal that message contains the primary address
2267 # rather than the actual address given to RCPT TO.
2268 message = ":fail: %s: %s" % (acc.get_primary_mailaddress(),
2269 message)
2270 et.email_target_alias = message
2271 et.write_db()
2272 return "OK, updated %s" % uname
2273
2274 # email edit_pipe_command <address> <command>
2275 all_commands['email_edit_pipe_command'] = Command(
2276 ("email", "edit_pipe_command"),
2277 EmailAddress(),
2278 SimpleString(help_ref="command_line"),
2279 perm_filter="can_email_pipe_edit")
2280 def email_edit_pipe_command(self, operator, addr, cmd):
2281 lp, dom = self._split_email_address(addr)
2282 ed = self._get_email_domain(dom)
2283 self.ba.can_email_pipe_edit(operator.get_entity_id(), ed)
2284 ea = Email.EmailAddress(self.db)
2285 try:
2286 ea.find_by_local_part_and_domain(lp, ed.entity_id)
2287 except Errors.NotFoundError:
2288 raise CerebrumError, "%s: No such address exists" % addr
2289 et = Email.EmailTarget(self.db)
2290 et.find(ea.email_addr_target_id)
2291 if not et.email_target_type in (self.const.email_target_pipe,
2292 self.const.email_target_RT):
2293 raise CerebrumError, "%s is not connected to a pipe or RT target" % addr
2294 if not cmd.startswith('|'):
2295 cmd = '|' + cmd
2296 if et.email_target_type == self.const.email_target_RT and \
2297 not re.match(self._rt_patt, cmd):
2298 raise CerebrumError("'%s' is not a valid RT command" % cmd)
2299 et.email_target_alias = cmd
2300 et.write_db()
2301 return "OK, edited %s" % addr
2302
2303 # email edit_pipe_user <address> <uname>
2304 all_commands['email_edit_pipe_user'] = Command(
2305 ("email", "edit_pipe_user"),
2306 EmailAddress(),
2307 AccountName(),
2308 perm_filter="can_email_pipe_edit")
2309 def email_edit_pipe_user(self, operator, addr, uname):
2310 lp, dom = self._split_email_address(addr)
2311 ed = self._get_email_domain(dom)
2312 self.ba.can_email_pipe_edit(operator.get_entity_id(), ed)
2313 ea = Email.EmailAddress(self.db)
2314 try:
2315 ea.find_by_local_part_and_domain(lp, ed.entity_id)
2316 except Errors.NotFoundError:
2317 raise CerebrumError, "%s: No such address exists" % addr
2318 et = Email.EmailTarget(self.db)
2319 et.find(ea.email_addr_target_id)
2320 if not et.email_target_type in (self.const.email_target_pipe,
2321 self.const.email_target_RT):
2322 raise CerebrumError, "%s is not connected to a pipe or RT target" % addr
2323 et.email_target_using_uid = self._get_account(uname).entity_id
2324 et.write_db()
2325 return "OK, edited %s" % addr
2326
2327
2328 # email create_domain <domainname> <description>
2329 all_commands['email_create_domain'] = Command(
2330 ("email", "create_domain"),
2331 SimpleString(help_ref="email_domain"),
2332 SimpleString(help_ref="string_description"),
2333 perm_filter="can_email_domain_create")
2334 def email_create_domain(self, operator, domainname, desc):
2335 """Create e-mail domain."""
2336 self.ba.can_email_archive_delete(operator.get_entity_id())
2337 ed = Email.EmailDomain(self.db)
2338 # Domainnames need to be lowercase, both when creating as well
2339 # as looking for them.
2340 domainname = domainname.lower()
2341 try:
2342 ed.find_by_domain(domainname)
2343 raise CerebrumError, "%s: e-mail domain already exists" % domainname
2344 except Errors.NotFoundError:
2345 pass
2346 if len(desc) < 3:
2347 raise CerebrumError, "Please supply a better description"
2348 try:
2349 ed.populate(domainname, desc)
2350 except AttributeError, ae:
2351 raise CerebrumError(str(ae))
2352 ed.write_db()
2353 return "OK, domain '%s' created" % domainname
2354
2355
2356 # email delete_domain <domainname>
2357 all_commands['email_delete_domain'] = Command(
2358 ("email", "delete_domain"),
2359 SimpleString(help_ref="email_domain"),
2360 perm_filter="can_email_domain_create")
2361 def email_delete_domain(self, operator, domainname):
2362 """Delete an e-mail domain."""
2363 self.ba.can_email_archive_delete(operator.get_entity_id())
2364
2365 domainname = domainname.lower()
2366 ed = Email.EmailDomain(self.db)
2367 try:
2368 ed.find_by_domain(domainname)
2369 except Errors.NotFoundError:
2370 raise CerebrumError, "%s: No e-mail domain by that name" % domainname
2371
2372 ea = Email.EmailAddress(self.db)
2373 if ea.search(domain_id=ed.entity_id, fetchall=True):
2374 raise CerebrumError, "E-mail-domain '%s' has addresses; cannot delete" % domainname
2375
2376 eed = Email.EntityEmailDomain(self.db)
2377 if eed.list_affiliations(domain_id=ed.entity_id):
2378 raise CerebrumError, "E-mail-domain '%s' associated with OUs; cannot delete" % domainname
2379
2380 ed.delete()
2381 ed.write_db()
2382
2383 return "OK, domain '%s' deleted" % domainname
2384
2385
2386 # email domain_configuration on|off <domain> <category>+
2387 all_commands['email_domain_configuration'] = Command(
2388 ("email", "domain_configuration"),
2389 SimpleString(help_ref="on_or_off"),
2390 SimpleString(help_ref="email_domain"),
2391 SimpleString(help_ref="email_category", repeat=True),
2392 perm_filter="can_email_domain_create")
2393 def email_domain_configuration(self, operator, onoff, domainname, cat):
2394 """Change configuration for an e-mail domain."""
2395 self.ba.can_email_domain_create(operator.get_entity_id())
2396 ed = self._get_email_domain(domainname)
2397 on = self._get_boolean(onoff)
2398 catcode = None
2399 for c in self.const.fetch_constants(self.const.EmailDomainCategory,
2400 prefix_match=cat):
2401 if catcode:
2402 raise CerebrumError, ("'%s' does not uniquely identify "+
2403 "a configuration category") % cat
2404 catcode = c
2405 if catcode is None:
2406 raise CerebrumError, ("'%s' does not match any configuration "+
2407 "category") % cat
2408 if self._sync_category(ed, catcode, on):
2409 return "%s is now %s" % (catcode, onoff.lower())
2410 else:
2411 return "%s unchanged" % catcode
2412
2413 # email domain_set_description
2414 all_commands['email_domain_set_description'] = Command(
2415 ("email", "domain_set_description"),
2416 SimpleString(help_ref="email_domain"),
2417 SimpleString(help_ref="string_description"),
2418 perm_filter='can_email_domain_create')
2419 def email_domain_set_description(self, operator, domainname, description):
2420 """Set the description of an e-mail domain."""
2421 self.ba.can_email_domain_create(operator.get_entity_id())
2422 ed = self._get_email_domain(domainname)
2423 ed.email_domain_description = description
2424 ed.write_db()
2425 return "OK, description for domain '%s' updated" % domainname
2426
2427 def _onoff(self, enable):
2428 if enable:
2429 return 'on'
2430 else:
2431 return 'off'
2432
2433 def _has_category(self, domain, category):
2434 ccode = int(category)
2435 for r in domain.get_categories():
2436 if r['category'] == ccode:
2437 return True
2438 return False
2439
2440 def _sync_category(self, domain, category, enable):
2441 """Enable or disable category with EmailDomain. Returns False
2442 for no change or True for change."""
2443 if self._has_category(domain, category) == enable:
2444 return False
2445 if enable:
2446 domain.add_category(category)
2447 else:
2448 domain.remove_category(category)
2449 return True
2450
2451 # email domain_info <domain>
2452 # this command is accessible for all
2453 all_commands['email_domain_info'] = Command(
2454 ("email", "domain_info"),
2455 SimpleString(help_ref="email_domain"),
2456 fs=FormatSuggestion([
2457 ("E-mail domain: %s\n"+
2458 "Description: %s",
2459 ("domainname", "description")),
2460 ("Configuration: %s",
2461 ("category",)),
2462 ("Affiliation: %s@%s",
2463 ("affil", "ou"))]))
2464 def email_domain_info(self, operator, domainname):
2465 ed = self._get_email_domain(domainname)
2466 ret = []
2467 ret.append({'domainname': domainname,
2468 'description': ed.email_domain_description})
2469 for r in ed.get_categories():
2470 ret.append({'category':
2471 str(self.const.EmailDomainCategory(r['category']))})
2472 eed = Email.EntityEmailDomain(self.db)
2473 affiliations = {}
2474 for r in eed.list_affiliations(ed.entity_id):
2475 ou = self._get_ou(r['entity_id'])
2476 affname = "<any>"
2477 if r['affiliation']:
2478 affname = str(self.const.PersonAffiliation(r['affiliation']))
2479 affiliations[self._format_ou_name(ou)] = affname
2480 aff_list = affiliations.keys()
2481 aff_list.sort()
2482 for ou in aff_list:
2483 ret.append({'affil': affiliations[ou], 'ou': ou})
2484 return ret
2485
2486 # email add_domain_affiliation <domain> <stedkode> [<affiliation>]
2487 all_commands['email_add_domain_affiliation'] = Command(
2488 ("email", "add_domain_affiliation"),
2489 SimpleString(help_ref="email_domain"),
2490 OU(), Affiliation(optional=True),
2491 perm_filter="can_email_domain_create")
2492 def email_add_domain_affiliation(self, operator, domainname, sko, aff=None):
2493 self.ba.can_email_domain_create(operator.get_entity_id())
2494 ed = self._get_email_domain(domainname)
2495 try:
2496 ou = self._get_ou(stedkode=sko)
2497 except Errors.NotFoundError:
2498 raise CerebrumError, "Unknown OU (%s)" % sko
2499 aff_id = None
2500 if aff:
2501 aff_id = int(self._get_affiliationid(aff))
2502 eed = Email.EntityEmailDomain(self.db)
2503 try:
2504 eed.find(ou.entity_id, aff_id)
2505 except Errors.NotFoundError:
2506 # We have a partially initialised object, since
2507 # the super() call finding the OU always succeeds.
2508 # Therefore we must not call clear()
2509 eed.populate_email_domain(ed.entity_id, aff_id)
2510 eed.write_db()
2511 count = self._update_email_for_ou(ou.entity_id, aff_id)
2512 # Perhaps we should return the values with a format
2513 # suggestion instead, but the message is informational,
2514 # and we have three different formats so it would be
2515 # awkward to do "right".
2516 return "OK, %d accounts updated" % count
2517 else:
2518 old_dom = eed.entity_email_domain_id
2519 if old_dom != ed.entity_id:
2520 eed.entity_email_domain_id = ed.entity_id
2521 eed.write_db()
2522 count = self._update_email_for_ou(ou.entity_id, aff_id)
2523 ed.clear()
2524 ed.find(old_dom)
2525 return "OK (was %s), %d accounts updated" % \
2526 (ed.email_domain_name, count)
2527 return "OK (no change)"
2528
2529 def _update_email_for_ou(self, ou_id, aff_id):
2530 """Updates the e-mail addresses for all accounts where the
2531 given affiliation is their primary, and returns the number of
2532 modified accounts."""
2533
2534 count = 0
2535 acc = self.Account_class(self.db)
2536 acc2 = self.Account_class(self.db)
2537 for r in acc.list_accounts_by_type(ou_id=ou_id, affiliation=aff_id):
2538 acc2.clear()
2539 acc2.find(r['account_id'])
2540 primary = acc2.get_account_types()[0]
2541 if (ou_id == primary['ou_id'] and
2542 (aff_id is None or aff_id == primary['affiliation'])):
2543 acc2.update_email_addresses()
2544 count += 1
2545 return count
2546
2547 # email remove_domain_affiliation <domain> <stedkode> [<affiliation>]
2548 all_commands['email_remove_domain_affiliation'] = Command(
2549 ("email", "remove_domain_affiliation"),
2550 SimpleString(help_ref="email_domain"),
2551 OU(), Affiliation(optional=True),
2552 perm_filter="can_email_domain_create")
2553 def email_remove_domain_affiliation(self, operator, domainname, sko,
2554 aff=None):
2555 self.ba.can_email_domain_create(operator.get_entity_id())
2556 ed = self._get_email_domain(domainname)
2557 try:
2558 ou = self._get_ou(stedkode=sko)
2559 except Errors.NotFoundError:
2560 raise CerebrumError, "Unknown OU (%s)" % sko
2561 aff_id = None
2562 if aff:
2563 aff_id = int(self._get_affiliationid(aff))
2564 eed = Email.EntityEmailDomain(self.db)
2565 try:
2566 eed.find(ou.entity_id, aff_id)
2567 except Errors.NotFoundError:
2568 raise CerebrumError, "No such affiliation for domain"
2569 if eed.entity_email_domain_id != ed.entity_id:
2570 raise CerebrumError, "No such affiliation for domain"
2571 eed.delete()
2572 return "OK, removed domain-affiliation for '%s'" % domainname
2573
2574 # email create_forward_target <local-address> <remote-address>
2575 all_commands['email_create_forward_target'] = Command(
2576 ("email", "create_forward_target"),
2577 EmailAddress(),
2578 EmailAddress(help_ref='email_forward_address'),
2579 perm_filter="can_email_forward_create")
2580 def email_create_forward_target(self, operator, localaddr, remoteaddr):
2581 """Create a forward target, add localaddr as an address
2582 associated with that target, and add remoteaddr as a forward
2583 addresses."""
2584 lp, dom = self._split_email_address(localaddr)
2585 ed = self._get_email_domain(dom)
2586 self.ba.can_email_forward_create(operator.get_entity_id(), ed)
2587 ea = Email.EmailAddress(self.db)
2588 try:
2589 ea.find_by_local_part_and_domain(lp, ed.entity_id)
2590 except Errors.NotFoundError:
2591 pass
2592 else:
2593 raise CerebrumError, "Address %s already exists" % localaddr
2594 et = Email.EmailTarget(self.db)
2595 et.populate(self.const.email_target_forward)
2596 et.write_db()
2597 ea.clear()
2598 ea.populate(lp, ed.entity_id, et.entity_id)
2599 ea.write_db()
2600 epat = Email.EmailPrimaryAddressTarget(self.db)
2601 epat.populate(ea.entity_id, parent=et)
2602 epat.write_db()
2603 ef = Email.EmailForward(self.db)
2604 ef.find(et.entity_id)
2605 addr = self._check_email_address(remoteaddr)
2606 try:
2607 ef.add_forward(addr)
2608 except Errors.TooManyRowsError:
2609 raise CerebrumError, "Forward address added already (%s)" % addr
2610 self._register_spam_settings(localaddr, self.const.email_target_forward)
2611 self._register_filter_settings(localaddr, self.const.email_target_forward)
2612 return "OK, created forward address '%s'" % localaddr
2613
2614
2615 def _register_spam_settings(self, address, target_type):
2616 """Register spam settings (level/action) associated with an address."""
2617
2618 et, addr = self._get_email_target_and_address(address)
2619 esf = Email.EmailSpamFilter(self.db)
2620 all_targets = [et.entity_id]
2621 if target_type == self.const.email_target_Sympa:
2622 all_targets = self._get_all_related_maillist_targets(addr.get_address())
2623 elif target_type == self.const.email_target_RT:
2624 all_targets = self._get_all_related_rt_targets(addr.get_address())
2625 target_type = str(target_type)
2626 if cereconf.EMAIL_DEFAULT_SPAM_SETTINGS.has_key(target_type):
2627 sl, sa = cereconf.EMAIL_DEFAULT_SPAM_SETTINGS[target_type]
2628 spam_level = int(self.const.EmailSpamLevel(sl))
2629 spam_action = int(self.const.EmailSpamAction(sa))
2630 for target_id in all_targets:
2631 et.clear()
2632 et.find(target_id)
2633 esf.clear()
2634 esf.populate(spam_level, spam_action, parent=et)
2635 esf.write_db()
2636 # end _register_spam_settings
2637
2638
2639 def _register_filter_settings(self, address, target_type):
2640 """Register spam filter settings associated with an address."""
2641 et, addr = self._get_email_target_and_address(address)
2642 etf = Email.EmailTargetFilter(self.db)
2643 all_targets = [et.entity_id]
2644 if target_type == self.const.email_target_Sympa:
2645 all_targets = self._get_all_related_maillist_targets(addr.get_address())
2646 elif target_type == self.const.email_target_RT:
2647 all_targets = self._get_all_related_rt_targets(addr.get_address())
2648 target_type = str(target_type)
2649 if cereconf.EMAIL_DEFAULT_FILTERS.has_key(target_type):
2650 for f in cereconf.EMAIL_DEFAULT_FILTERS[target_type]:
2651 filter_code = int(self.const.EmailTargetFilter(f))
2652 for target_id in all_targets:
2653 et.clear()
2654 et.find(target_id)
2655 etf.clear()
2656 etf.populate(filter_code, parent=et)
2657 etf.write_db()
2658 # end _register_filter_settings
2659
2660 # email create_sympa_list run-host delivery-host <listaddr> adm prof desc
2661 all_commands['email_create_sympa_list'] = Command(
2662 ("email", "create_sympa_list"),
2663 SimpleString(help_ref='string_exec_host'),
2664 SimpleString(help_ref='string_email_delivery_host'),
2665 EmailAddress(help_ref="mailing_list"),
2666 SimpleString(help_ref="mailing_admins"),
2667 SimpleString(help_ref="mailing_list_profile"),
2668 SimpleString(help_ref="mailing_list_description"),
2669 YesNo(help_ref="yes_no_force", optional=True, default="No"),
2670 perm_filter="can_email_list_create")
2671 def email_create_sympa_list(self, operator, run_host, delivery_host,
2672 listname, admins, list_profile,
2673 list_description, force=None):
2674 """Create a sympa list in Cerebrum and on the sympa server(s).
2675
2676 Register all the necessary cerebrum information and make a bofhd
2677 request for the actual list creation.
2678 """
2679
2680 # Check that the profile is legal
2681 if list_profile not in cereconf.SYMPA_PROFILES:
2682 raise CerebrumError("Profile %s for sympa list %s is not valid" %
2683 (list_profile, listname))
2684
2685 # Check that the command exec host is sane
2686 if run_host not in cereconf.SYMPA_RUN_HOSTS:
2687 raise CerebrumError("run-host %s for sympa list %s is not valid" %
2688 (run_host, listname))
2689
2690 metachars = "'\"$&()*;<>?[\\]`{|}~\n"
2691 def has_meta(s1, s2=metachars):
2692 """Check if any char of s1 is in s2"""
2693 for c in s1:
2694 if c in s2:
2695 return True
2696 return False
2697 # end any
2698
2699 # Sympa list creation command will be passed through multiple
2700 # exec/shells. Better be restrictive.
2701 if True in [has_meta(x) for x in
2702 (run_host, delivery_host, listname, admins, list_profile,
2703 list_description)]:
2704 raise CerebrumError("Illegal metacharacter in list parameter. None "
2705 "of the %s are allowed." % metachars)
2706
2707 delivery_host = self._get_email_server(delivery_host)
2708 if self._is_yes(force):
2709 self._create_mailing_list_in_cerebrum(operator,
2710 self.const.email_target_Sympa,
2711 delivery_host,
2712 listname, force=True)
2713 else:
2714 self._create_mailing_list_in_cerebrum(operator,
2715 self.const.email_target_Sympa,
2716 delivery_host,
2717 listname)
2718 # Now make a bofhd request to create the list itself
2719 admin_list = list()
2720 for item in admins.split(","):
2721 # it's a user name. That username must exist in Cerebrum
2722 if "@" not in item:
2723 self._get_account(item)
2724 item = item + "@ulrik.uit.no"
2725 admin_list.append(item)
2726
2727 # Make the request.
2728 lp, dom = self._split_email_address(listname)
2729 ed = self._get_email_domain(dom)
2730 ea = Email.EmailAddress(self.db)
2731 ea.clear()
2732 ea.find_by_local_part_and_domain(lp, ed.entity_id)
2733 list_id = ea.entity_id
2734 # IVR 2008-08-01 TBD: this is a big ugly. We need to pass several
2735 # arguments to p_b_r, but we cannot really store them anywhere :( The
2736 # idea is then to take a small dict, pickle it, shove into state_data,
2737 # unpickle in p_b_r and be on our merry way. It is at the very best
2738 # suboptimal.
2739 state = {"runhost": run_host, # IVR 2008-08-01 FIXME: non-fqdn? force?
2740 # check?
2741 "admins": admin_list,
2742 "profile": list_profile,
2743 "description": list_description,
2744 }
2745 br = BofhdRequests(self.db, self.const)
2746
2747 # IVR 2009-04-17 +30 minute delay to allow changes to spread to
2748 # LDAP. The postmasters are nagging for that delay. All questions
2749 # should be directed to them (this is similar to delaying a delete
2750 # request).
2751 br.add_request(operator.get_entity_id(),
2752 DateTime.now() + DateTime.DateTimeDelta(0, 0, 30),
2753 self.const.bofh_sympa_create, list_id, ea.entity_id,
2754 state_data=pickle.dumps(state))
2755 return "OK, sympa list '%s' created" % listname
2756
2757 all_commands['email_create_sympa_cerebrum_list'] = Command(
2758 ("email", "create_sympa_cerebrum_list"),
2759 SimpleString(help_ref='string_email_delivery_host'),
2760 EmailAddress(help_ref="mailing_list"),
2761 YesNo(help_ref="yes_no_force", optional=True, default="No"),
2762 perm_filter="can_email_list_create")
2763 def email_create_sympa_cerebrum_list(self, operator, delivery_host, listname, force=None):
2764 """Create a sympa mailing list in cerebrum only"""
2765
2766 delivery_host = self._get_email_server(delivery_host)
2767 if self._is_yes(force):
2768 self._create_mailing_list_in_cerebrum(operator,
2769 self.const.email_target_Sympa,
2770 delivery_host,
2771 listname, force=True)
2772 else:
2773 self._create_mailing_list_in_cerebrum(operator,
2774 self.const.email_target_Sympa,
2775 delivery_host,
2776 listname)
2777 return "OK, sympa list '%s' created in cerebrum only" % listname
2778
2779 def _create_mailing_list_in_cerebrum(self, operator, target_type,
2780 delivery_host, listname, force=False):
2781 """Register cerebrum information (only) about a new mailing list.
2782
2783 @type target_type: an EmailTarget constant
2784 @param target_type:
2785 ET specifying the mailing list we are creating.
2786
2787 @type admins: basestring
2788 @param admins:
2789 This one is a tricky bugger. This is either a single value or a
2790 sequence thereof. If it is a sequence, then the items are separated
2791 by commas.
2792
2793 Each item is either a user name, or an e-mail address. User names
2794 *MUST* exist in Cerebrum and *MUST* have e-mail addresses. E-mail
2795 addresses do NOT have to be registered in Cerebrum (they may, in
2796 fact, be external to Cerebrum).
2797
2798 @type force: boolean.
2799 @param force:
2800 If True, *force* certain operations.
2801 """
2802
2803 local_part, domain = self._split_email_address(listname)
2804 ed = self._get_email_domain(domain)
2805 operator_id = operator.get_entity_id()
2806 self.ba.can_email_list_create(operator_id, ed)
2807 email_address = Email.EmailAddress(self.db)
2808 # First, check whether the address already exists
2809 try:
2810 email_address.find_by_local_part_and_domain(local_part,
2811 ed.entity_id)
2812 except Errors.NotFoundError:
2813 pass
2814 else:
2815 raise CerebrumError("Mail address %s already exists" % listname)
2816
2817 # Then, check whether there is a user name equal to local_part.
2818 try:
2819 self._get_account(local_part)
2820 except CerebrumError:
2821 pass
2822 else:
2823 if not (local_part in ("drift",) or
2824 (self.ba.is_postmaster(operator_id) and force)):
2825 # TBD: This exception list should probably not be hardcoded
2826 # here -- but it's not obvious whether it should be a cereconf
2827 # value (implying that only site admins can modify the list)
2828 # or a database table.
2829 raise CerebrumError("%s is an existing username" % local_part)
2830
2831 # Then check whether the mailing list name is a legal one.
2832 if not (self._is_ok_mailing_list_name(local_part) or
2833 self.ba.is_postmaster(operator_id)):
2834 raise CerebrumError("Illegal mailing list name: %s" % listname)
2835
2836 # Finally, we can start registering useful stuff
2837 # Register all relevant addresses for the list...
2838 if target_type == self.const.email_target_Sympa:
2839 self._register_sympa_list_addresses(listname, local_part, domain,
2840 delivery_host)
2841 else:
2842 raise CerebrumError("Unknown mail list target: %s" % target_type)
2843 # register auto spam and filter settings for the list
2844 self._register_spam_settings(listname, target_type)
2845 self._register_filter_settings(listname, target_type)
2846
2847 # email create_sympa_list_alias <list-address> <new-alias>
2848 all_commands['email_create_sympa_list_alias'] = Command(
2849 ("email", "create_sympa_list_alias"),
2850 EmailAddress(help_ref="mailing_list_exist"),
2851 EmailAddress(help_ref="mailing_list"),
2852 YesNo(help_ref="yes_no_force", optional=True),
2853 perm_filter="can_email_list_create")
2854 def email_create_sympa_list_alias(self, operator, listname, address, force=False):
2855 """Create a secondary name for an existing Sympa list."""
2856 if isinstance(force, str):
2857 force = self._get_boolean(force)
2858 # The first thing we have to do is to locate the delivery
2859 # host. Postmasters do NOT want to allow people to specify a different
2860 # delivery host for alias than for the list that is being aliased. So,
2861 # find the ml's ET and fish out the server_id.
2862 self._validate_sympa_list(listname)
2863 local_part, domain = self._split_email_address(listname)
2864 ed = self._get_email_domain(domain)
2865 email_address = Email.EmailAddress(self.db)
2866 email_address.find_by_local_part_and_domain(local_part,
2867 ed.entity_id)
2868 try:
2869 et = Email.EmailTarget(self.db)
2870 et.find(email_address.email_addr_target_id)
2871 delivery_host = Email.EmailServer(self.db)
2872 delivery_host.find(et.email_server_id)
2873 except Errors.NotFoundError:
2874 raise CerebrumError("Cannot alias list %s (missing delivery host)",
2875 listname)
2876
2877 return self._create_list_alias(operator, listname, address,
2878 self.const.email_target_Sympa,
2879 delivery_host, force_alias=force)
2880
2881 def _create_list_alias(self, operator, listname, address, list_type,
2882 delivery_host, force_alias=False):
2883 """Create an alias `address` for an existing mailing list `listname`.
2884
2885 :type listname: basestring
2886 :param listname:
2887 Email address for an existing mailing list. This is the mailing
2888 list we are aliasing.
2889
2890 :type address: basestring
2891 :param address: Email address which will be the alias.
2892
2893 :type list_type: _EmailTargetCode instance
2894 :param list_type: List type we are processing.
2895
2896 :type delivery_host: EmailServer instance or None.
2897 :param delivery_host:
2898 Host where delivery to the mail alias happens. It is the
2899 responsibility of the caller to check that this value makes sense in
2900 the context of the specified mailing list.
2901 """
2902
2903 if list_type != self.const.email_target_Sympa:
2904 raise CerebrumError("Unknown list type %s for list %s" %
2905 (self.const.EmailTarget(list_type), listname))
2906 lp, dom = self._split_email_address(address)
2907 ed = self._get_email_domain(dom)
2908 self.ba.can_email_list_create(operator.get_entity_id(), ed)
2909 self._validate_sympa_list(listname)
2910 if not force_alias:
2911 try:
2912 self._get_account(lp)
2913 except CerebrumError:
2914 pass
2915 else:
2916 raise CerebrumError, ("Won't create list-alias %s, as %s is an "
2917 "existing username") % (address, lp)
2918 self._register_sympa_list_addresses(listname, lp, dom, delivery_host)
2919 return "OK, list-alias '%s' created" % address
2920
2921 def _report_deleted_EA(self, deleted_EA):
2922 """Send a message to postmasters informing them that a number of email
2923 addresses are about to be deleted.
2924
2925 postmasters requested on 2009-08-19 that they want to be informed when
2926 an e-mail list's aliases are being deleted (to have a record, in case
2927 the operation is to be reversed). The simplest solution is to send an
2928 e-mail informing them when something is deleted.
2929 """
2930
2931 if not deleted_EA:
2932 return
2933
2934 def email_info2string(EA):
2935 """Map whatever email_info returns to something human-friendly"""
2936
2937 def dict2line(d):
2938 filtered_keys = ("spam_action_desc", "spam_level_desc",)
2939 return "\n".join("%s: %s" % (str(key), str(d[key]))
2940 for key in d
2941 if key not in filtered_keys)
2942
2943 result = list()
2944 for item in EA:
2945 if isinstance(item, dict):
2946 result.append(dict2line(item))
2947 else:
2948 result.append(repr(item))
2949
2950 return "\n".join(result)
2951 # end email_info2string
2952
2953 to_address = "bas-admin@cc.uit.no"
2954 from_address = "bas-admin@cc.uit.no"
2955 try:
2956 Utils.sendmail(toaddr=to_address,
2957 fromaddr=from_address,
2958 subject="Removal of e-mail addresses in Cerebrum",
2959 body="""
2960This is an automatically generated e-mail.
2961
2962The following e-mail list addresses have just been removed from Cerebrum. Keep
2963this message, in case a restore is requested later.
2964
2965Addresses and settings:
2966
2967%s
2968 """ % email_info2string(deleted_EA))
2969
2970 # We don't want this function ever interfering with bofhd's
2971 # operation. If it fails -- screw it.
2972 except:
2973 self.logger.info("Failed to send e-mail to %s", to_address)
2974 self.logger.info("Failed e-mail info: %s", repr(deleted_EA))
2975 # end _report_deleted_EA
2976
2977
2978
2979 # email remove_sympa_list_alias <alias>
2980 all_commands['email_remove_sympa_list_alias'] = Command(
2981 ('email', 'remove_sympa_list_alias'),
2982 EmailAddress(help_ref='mailing_list_alias'),
2983 perm_filter='can_email_list_create')
2984 def email_remove_sympa_list_alias(self, operator, alias):
2985 lp, dom = self._split_email_address(alias, with_checks=False)
2986 ed = self._get_email_domain(dom)
2987 remove_addrs = [alias]
2988 self.ba.can_email_list_create(operator.get_entity_id(), ed)
2989 ea = Email.EmailAddress(self.db)
2990 et = Email.EmailTarget(self.db)
2991
2992 for addr_format, pipe in self._sympa_addr2alias:
2993 addr = addr_format % {"local_part": lp,
2994 "domain": dom,}
2995 try:
2996 ea.clear()
2997 ea.find_by_address(addr)
2998 except Errors.NotFoundError:
2999 # Even if one of the addresses is missing, it does not matter
3000 # -- we are removing the alias anyway. The right thing to do
3001 # here is to continue, as if deletion worked fine. Note that
3002 # the ET belongs to the original address, not the alias, so if
3003 # we don't delete it when the *alias* is removed, we should
3004 # still be fine.
3005 continue
3006
3007 try:
3008 et.clear()
3009 et.find(ea.email_addr_target_id)
3010 except Errors.NotFoundError:
3011 raise CerebrumError("Could not find e-mail target for %s" %
3012 addr)
3013
3014 # nuke the address, and, if it's the last one, nuke the target as
3015 # well.
3016 self._remove_email_address(et, addr)
3017 return "OK, removed alias %s and all auto registered aliases" % alias
3018
3019 # email delete_sympa_list <run-host> <list-address>
3020 all_commands['email_delete_sympa_list'] = Command(
3021 ("email", "delete_sympa_list"),
3022 SimpleString(help_ref='string_exec_host'),
3023 EmailAddress(help_ref="mailing_list_exist"),
3024 YesNo(help_ref="yes_no_with_request"),
3025 fs=FormatSuggestion([("Deleted address: %s", ("address", ))]),
3026 perm_filter="can_email_list_delete")
3027 def email_delete_sympa_list(self, operator, run_host, listname,
3028 force_request):
3029 """Remove a sympa list from cerebrum.
3030
3031 @type force_request: bool
3032 @param force_request:
3033 Controls whether a bofhd request should be issued. This may come in
3034 handy, if we want to delete a sympa list from Cerebrum only and not
3035 issue any requests. misc cancel_request would have worked too, but
3036 it's better to merge this into one command.
3037 """
3038
3039 # Check that the command exec host is sane
3040 if run_host not in cereconf.SYMPA_RUN_HOSTS:
3041 raise CerebrumError("run-host %s for sympa list %s is not valid" %
3042 (run_host, listname))
3043
3044 et, ea = self._get_email_target_and_address(listname)
3045 self.ba.can_email_list_delete(operator.get_entity_id(), ea)
3046
3047 if et.email_target_type != self.const.email_target_Sympa:
3048 raise CerebrumError("email delete_sympa works on sympa lists only. "
3049 "'%s' is not a sympa list (%s)" %
3050 (listname,
3051 self.const.EmailTarget(et.email_target_type)))
3052
3053 epat = Email.EmailPrimaryAddressTarget(self.db)
3054 list_id = ea.entity_id
3055 # Now, there are *many* ETs/EAs associated with one sympa list. We
3056 # have to wipe them all out.
3057 if not self._validate_sympa_list(listname):
3058 raise CerebrumError("Illegal sympa list name: '%s'", listname)
3059
3060 deleted_EA = self.email_info(operator, listname)
3061 # needed for pattern interpolation below (these are actually used)
3062 local_part, domain = self._split_email_address(listname)
3063 for pattern, pipe_destination in self._sympa_addr2alias:
3064 address = pattern % locals()
3065 # For each address, find the target, and remove all email
3066 # addresses for that target (there may be many addresses for the
3067 # same target).
3068 try:
3069 ea.clear()
3070 ea.find_by_address(address)
3071 et.clear()
3072 et.find(ea.get_target_id())
3073 epat.clear()
3074 try:
3075 epat.find(et.entity_id)
3076 except Errors.NotFoundError:
3077 pass
3078 else:
3079 epat.delete()
3080 # Wipe all addresses...
3081 for row in et.get_addresses():
3082 addr = '%(local_part)s@%(domain)s' % row
3083 ea.clear()
3084 ea.find_by_address(addr)
3085 ea.delete()
3086 et.delete()
3087 except Errors.NotFoundError:
3088 pass
3089
3090 if cereconf.INSTITUTION_DOMAIN_NAME == 'uit.no':
3091 self._report_deleted_EA(deleted_EA)
3092 if not self._is_yes(force_request):
3093 return "OK, sympa list '%s' deleted (no bofhd request)" % listname
3094
3095 br = BofhdRequests(self.db, self.const)
3096 state = {'run_host': run_host,
3097 'listname': listname}
3098 br.add_request(operator.get_entity_id(),
3099 # IVR 2008-08-04 +1 hour to allow changes to spread to
3100 # LDAP. This way we'll have a nice SMTP-error, rather
3101 # than a confusing error burp from sympa.
3102 DateTime.now() + DateTime.DateTimeDelta(0, 1),
3103 self.const.bofh_sympa_remove,
3104 list_id, None, state_data=pickle.dumps(state))
3105
3106 return "OK, sympa list '%s' deleted (bofhd request issued)" % listname
3107
3108 def _split_email_address(self, addr, with_checks=True):
3109 """Split an e-mail address into local part and domain.
3110
3111 Additionally, perform certain basic checks to ensure that the address
3112 looks sane.
3113
3114 @type addr: basestring
3115 @param addr:
3116 E-mail address to split, spelled as 'foo@domain'.
3117
3118 @type with_checks: bool
3119 @param with_checks:
3120 Controls whether to perform local part checks on the
3121 address. Occasionally we may want to sidestep this (e.g. when
3122 *removing* things from the database).
3123
3124 @rtype: tuple of (basestring, basestring)
3125 @return:
3126 A pair, local part and domain extracted from the L{addr}.
3127 """
3128
3129 if addr.count('@') == 0:
3130 raise CerebrumError, \
3131 "E-mail address (%s) must include domain" % addr
3132 lp, dom = addr.split('@')
3133 if addr != addr.lower() and \
3134 dom not in cereconf.LDAP['rewrite_email_domain']:
3135 raise CerebrumError, \
3136 "E-mail address (%s) can't contain upper case letters" % addr
3137
3138 if not with_checks:
3139 return lp, dom
3140
3141 ea = Email.EmailAddress(self.db)
3142 if not ea.validate_localpart(lp):
3143 raise CerebrumError, "Invalid localpart '%s'" % lp
3144 return lp, dom
3145
3146 def _validate_sympa_list(self, listname):
3147 """Check whether `listname` is the 'official' name for a Sympa mailing
3148 list.
3149
3150 Raise an error, if it is not.
3151 """
3152 if self._get_sympa_list(listname) != listname:
3153 raise CerebrumError("%s is NOT the official Sympa list name" %
3154 listname)
3155 return listname
3156
3157 def _get_sympa_list(self, listname):
3158 """Try to return the 'official' sympa mailing list name, if it can at
3159 all be derived from listname.
3160
3161 The problem here is that some lists are actually called
3162 foo-admin@domain (and their admin address is foo-admin-admin@domain).
3163
3164 Since the 'official' names are not tagged in any way, we try to
3165 guess. The guesswork proceeds as follows:
3166
3167 1) if listname points to a sympa ET that has a primary address, we are
3168 done, listname *IS* the official list name
3169 2) if not, then there must be a prefix/suffix (like -request) and if
3170 we chop it off, we can checked the chopped off part for being an
3171 official sympa list. The chopping off continues until we run out of
3172 special prefixes/suffixes.
3173 """
3174
3175 ea = Email.EmailAddress(self.db)
3176 et = Email.EmailTarget(self.db)
3177 epat = Email.EmailPrimaryAddressTarget(self.db)
3178 def has_prefix(address):
3179 local_part, domain = self._split_email_address(address)
3180 return True in [local_part.startswith(x)
3181 for x in self._sympa_address_prefixes]
3182
3183 def has_suffix(address):
3184 local_part, domain = self._split_email_address(address)
3185 return True in [local_part.endswith(x)
3186 for x in self._sympa_address_suffixes]
3187
3188 def has_primary_to_me(address):
3189 try:
3190 ea.clear()
3191 ea.find_by_address(address)
3192 epat.clear()
3193 epat.find(ea.get_target_id())
3194 return True
3195 except Errors.NotFoundError:
3196 return False
3197
3198 def I_am_sympa(address, check_suffix_prefix=True):
3199 try:
3200 ea.clear()
3201 ea.find_by_address(address)
3202 except Errors.NotFoundError:
3203 # If it does not exist, it cannot be sympa
3204 return False
3205
3206 et.clear()
3207 et.find(ea.get_target_id())
3208 if (not et.email_target_alias or
3209 et.email_target_type != self.const.email_target_Sympa):
3210 # if it's not a Sympa ET, address cannot be sympa
3211 return False
3212
3213 return True
3214 # end I_am_sympa
3215
3216 not_sympa_error = CerebrumError("%s is not a Sympa list" % listname)
3217 # Simplest case -- listname is actually a sympa ML directly. It does
3218 # not matter whether it has a funky prefix/suffix.
3219 if I_am_sympa(listname) and has_primary_to_me(listname):
3220 return listname
3221
3222 # However, if listname does not have a prefix/suffix AND it is not a
3223 # sympa address with a primary address, them it CANNOT be a sympa
3224 # address.
3225 if not (has_prefix(listname) or has_suffix(listname)):
3226 raise not_sympa_error
3227
3228 # There is a funky suffix/prefix. Is listname actually such a
3229 # secondary address? Try to chop off the funky part and test.
3230 local_part, domain = self._split_email_address(listname)
3231 for prefix in self._sympa_address_prefixes:
3232 if not local_part.startswith(prefix):
3233 continue
3234
3235 lp_tmp = local_part[len(prefix):]
3236 addr_to_test = lp_tmp + "@" + domain
3237 try:
3238 self._get_sympa_list(addr_to_test)
3239 return addr_to_test
3240 except CerebrumError:
3241 pass
3242
3243 for suffix in self._sympa_address_suffixes:
3244 if not local_part.endswith(suffix):
3245 continue
3246
3247 lp_tmp = local_part[:-len(suffix)]
3248 addr_to_test = lp_tmp + "@" + domain
3249 try:
3250 self._get_sympa_list(addr_to_test)
3251 return addr_to_test
3252 except CerebrumError:
3253 pass
3254
3255 raise not_sympa_error
3256
3257 def _get_all_related_maillist_targets(self, address):
3258 """This method locates and returns all ETs associated with the same ML.
3259
3260 Given any address associated with a ML, this method returns all the
3261 ETs associated with that ML. E.g.: 'foo-subscribe@domain' for a Sympa
3262 ML will result in returning the ETs for 'foo@domain',
3263 'foo-owner@domain', 'foo-request@domain', 'foo-editor@domain',
3264 'foo-subscribe@domain' and 'foo-unsubscribe@domain'
3265
3266 If address (EA) is not associated with a mailing list ET, this method
3267 raises an exception. Otherwise a list of ET entity_ids is returned.
3268
3269 @type address: basestring
3270 @param address:
3271 One of the mail addresses associated with a mailing list.
3272
3273 @rtype: sequence (of ints)
3274 @return:
3275 A sequence with entity_ids of all ETs related to the ML that address
3276 is related to.
3277
3278 """
3279
3280 # step 1, find the ET, check its type.
3281 et, ea = self._get_email_target_and_address(address)
3282 # Mapping from ML types to (x, y)-tuples, where x is a callable that
3283 # fetches the ML's official/main address, and y is a set of patterns
3284 # for EAs that are related to this ML.
3285 ml2action = {
3286 int(self.const.email_target_Sympa):
3287 (self._get_sympa_list, [x[0] for x in self._sympa_addr2alias]),
3288 }
3289
3290 if int(et.email_target_type) not in ml2action:
3291 raise CerebrumError("'%s' is not associated with a mailing list" %
3292 address)
3293
3294 result = []
3295 get_official_address, patterns = ml2action[int(et.email_target_type)]
3296 # step 1, get official ML address (i.e. foo@domain)
3297 official_ml_address = get_official_address(ea.get_address())
3298 ea.clear()
3299 ea.find_by_address(official_ml_address)
3300 et.clear()
3301 et.find(ea.get_target_id())
3302
3303 # step 2, get local_part and domain separated:
3304 local_part, domain = self._split_email_address(official_ml_address)
3305
3306 # step 3, generate all 'derived'/'administrative' addresses, and
3307 # locate their ETs.
3308 result = set([et.entity_id,])
3309 for pattern in patterns:
3310 address = pattern % {"local_part": local_part, "domain": domain}
3311
3312 # some of the addresses may be missing. It is not an error.
3313 try:
3314 ea.clear()
3315 ea.find_by_address(address)
3316 except Errors.NotFoundError:
3317 continue
3318
3319 result.add(ea.get_target_id())
3320
3321 return result
3322
3323 def _is_ok_mailing_list_name(self, localpart):
3324 # originally this regexp was:^[a-z0-9.-]. postmaster however
3325 # needs to be able to recreate some of the older mailing lists
3326 # in sympa and '_' used to be a valid character in list names.
3327 # this may not be very wise, but the postmasters have promised
3328 # to be good and make sure not to abuse this :-). Jazz,
3329 # 2009-11-13
3330 if not re.match(r'^[a-z0-9.-]+$|^[a-z0-9._]+$', localpart):
3331 raise CerebrumError, "Illegal localpart: %s" % localpart
3332 if len(localpart) > 8 or localpart.count('-') or localpart == 'drift':
3333 return True
3334 return False
3335
3336 # aliases that we must create for each sympa mailing list.
3337 # request,editor,-owner,subscribe,unsubscribe all come from sympa
3338 # owner- and -admin are the remnants of mailman
3339 _sympa_addr2alias = (
3340 # The first one *is* the official/primary name. Don't reshuffle.
3341 ('%(local_part)s@%(domain)s', "|SYMPA_QUEUE %(listname)s"),
3342 # Owner addresses...
3343 ('%(local_part)s-owner@%(domain)s', "|SYMPA_BOUNCEQUEUE %(listname)s"),
3344 ('%(local_part)s-admin@%(domain)s', "|SYMPA_BOUNCEQUEUE %(listname)s"),
3345 # Request addresses...
3346 ('%(local_part)s-request@%(domain)s',
3347 "|SYMPA_QUEUE %(local_part)s-request@%(domain)s"),
3348 ('owner-%(local_part)s@%(domain)s',
3349 "|SYMPA_QUEUE %(local_part)s-request@%(domain)s"),
3350 # Editor address...
3351 ('%(local_part)s-editor@%(domain)s',
3352 "|SYMPA_QUEUE %(local_part)s-editor@%(domain)s"),
3353 # Subscribe address...
3354 ('%(local_part)s-subscribe@%(domain)s',
3355 "|SYMPA_QUEUE %(local_part)s-subscribe@%(domain)s"),
3356 # Unsubscribe address...
3357 ('%(local_part)s-unsubscribe@%(domain)s',
3358 "|SYMPA_QUEUE %(local_part)s-unsubscribe@%(domain)s"),
3359 )
3360 _sympa_address_suffixes = ("-owner", "-admin", "-request", "-editor",
3361 "-subscribe", "-unsubscribe",)
3362 _sympa_address_prefixes = ("owner-",)
3363
3364 def _register_sympa_list_addresses(self, listname, local_part, domain,
3365 delivery_host):
3366 """Add list, request, editor, owner, subscribe and unsubscribe
3367 addresses to a sympa mailing list.
3368
3369 :type listname: basestring
3370 :param listname:
3371 Sympa listname that the operation is about. listname is typically
3372 different from local_part@domain when we are creating an
3373 alias. local_part@domain is the alias, listname is the original
3374 listname. And since aliases should point to the 'original' ETs, we
3375 have to use listname to locate the ETs.
3376
3377 :type local_part: basestring
3378 :param local_part: See domain
3379
3380 :type domain: basestring
3381 :param domain:
3382 `local_part` and `domain` together represent a new list address that
3383 we want to create.
3384
3385 @type delivery_host: EmailServer instance.
3386 @param delivery_host:
3387 EmailServer where e-mail to `listname` is to be delivered through.
3388 """
3389
3390 if (delivery_host.email_server_type !=
3391 self.const.email_server_type_sympa):
3392 raise CerebrumError("Delivery host %s has wrong type %s for "
3393 "sympa ML %s" %
3394 (delivery_host.get_name(self.const.host_namespace),
3395 self.const.EmailServerType(delivery_host.email_server_type),
3396 listname))
3397
3398 ed = Email.EmailDomain(self.db)
3399 ed.find_by_domain(domain)
3400
3401 et = Email.EmailTarget(self.db)
3402 ea = Email.EmailAddress(self.db)
3403 epat = Email.EmailPrimaryAddressTarget(self.db)
3404 try:
3405 ea.find_by_local_part_and_domain(local_part, ed.entity_id)
3406 except Errors.NotFoundError:
3407 pass
3408 else:
3409 raise CerebrumError, ("The address %s@%s is already in use" %
3410 (local_part, domain))
3411
3412 sympa = self._get_account("sympa", actype="PosixUser")
3413 primary_ea_created= False
3414 listname_lp, listname_domain = listname.split("@")
3415
3416 # For each of the addresses we are supposed to create...
3417 for pattern, pipe_destination in self._sympa_addr2alias:
3418 address = pattern % locals()
3419 address_lp, address_domain = address.split("@")
3420
3421 # pipe has to be derived from the original listname, since it's
3422 # used to locate the ET.
3423 pipe = pipe_destination % {"local_part": listname_lp,
3424 "domain": listname_domain,
3425 "listname": listname}
3426
3427 # First check whether the address already exist. It should not.
3428 try:
3429 ea.clear()
3430 ea.find_by_local_part_and_domain(address_lp, ed.entity_id)
3431 raise CerebrumError("Can't add list %s as the address %s "
3432 "is already in use" % (listname,
3433 address))
3434 except Errors.NotFoundError:
3435 pass
3436
3437 # Then find the target for this particular email address. The
3438 # target may already exist, though.
3439 et.clear()
3440 try:
3441 et.find_by_alias_and_account(pipe, sympa.entity_id)
3442 except Errors.NotFoundError:
3443 et.populate(self.const.email_target_Sympa,
3444 alias=pipe, using_uid=sympa.entity_id,
3445 server_id=delivery_host.entity_id)
3446 et.write_db()
3447
3448 # Then create the email address and associate it with the ET.
3449 ea.clear()
3450 ea.populate(address_lp, ed.entity_id, et.entity_id)
3451 ea.write_db()
3452
3453 # And finally, the primary address. The first entry in
3454 # _sympa_addr2alias will match. Do not reshuffle that tuple!
3455 if not primary_ea_created:
3456 epat.clear()
3457 try:
3458 epat.find(et.entity_id)
3459 except Errors.NotFoundError:
3460 epat.clear()
3461 epat.populate(ea.entity_id, parent=et)
3462 epat.write_db()
3463 primary_ea_created = True
3464 # end _register_sympa_list_addresses
3465
3466
3467 # email create_multi <multi-address> <group>
3468 all_commands['email_create_multi'] = Command(
3469 ("email", "create_multi"),
3470 EmailAddress(help_ref="email_address"),
3471 GroupName(help_ref="group_name_dest"),
3472 perm_filter="can_email_multi_create")
3473 def email_create_multi(self, operator, addr, group):
3474 """Create en e-mail target of type 'multi' expanding to
3475 members of group, and associate the e-mail address with this
3476 target."""
3477 lp, dom = self._split_email_address(addr)
3478 ed = self._get_email_domain(dom)
3479 gr = self._get_group(group)
3480 self.ba.can_email_multi_create(operator.get_entity_id(), ed, gr)
3481 ea = Email.EmailAddress(self.db)
3482 try:
3483 ea.find_by_local_part_and_domain(lp, ed.entity_id)
3484 except Errors.NotFoundError:
3485 pass
3486 else:
3487 raise CerebrumError, "Address <%s> is already in use" % addr
3488 et = Email.EmailTarget(self.db)
3489 et.populate(self.const.email_target_multi,
3490 target_entity_type = self.const.entity_group,
3491 target_entity_id = gr.entity_id)
3492 et.write_db()
3493 ea.clear()
3494 ea.populate(lp, ed.entity_id, et.entity_id)
3495 ea.write_db()
3496 epat = Email.EmailPrimaryAddressTarget(self.db)
3497 epat.populate(ea.entity_id, parent=et)
3498 epat.write_db()
3499 self._register_spam_settings(addr, self.const.email_target_multi)
3500 self._register_filter_settings(addr, self.const.email_target_multi)
3501 return "OK, multi-target for '%s' created" % addr
3502
3503 # email delete_multi <address>
3504 all_commands['email_delete_multi'] = Command(
3505 ("email", "delete_multi"),
3506 EmailAddress(help_ref="email_address"),
3507 fs=FormatSuggestion([("Deleted address: %s", ("address", ))]),
3508 perm_filter="can_email_multi_delete")
3509 def email_delete_multi(self, operator, addr):
3510 lp, dom = self._split_email_address(addr)
3511 ed = self._get_email_domain(dom)
3512 et, acc = self._get_email_target_and_account(addr)
3513 if et.email_target_type != self.const.email_target_multi:
3514 raise CerebrumError, "%s: Not a multi target" % addr
3515 if et.email_target_entity_type != self.const.entity_group:
3516 raise CerebrumError, "%s: Does not point to a group!" % addr
3517 gr = self._get_group(et.email_target_entity_id, idtype="id")
3518 self.ba.can_email_multi_delete(operator.get_entity_id(), ed, gr)
3519 epat = Email.EmailPrimaryAddressTarget(self.db)
3520 try:
3521 epat.find(et.entity_id)
3522 except Errors.NotFoundError:
3523 # a multi target does not need a primary address
3524 pass
3525 else:
3526 # but if one exists, we require the user to supply that
3527 # address, not an arbitrary alias.
3528 if addr != self._get_address(epat):
3529 raise CerebrumError, ("%s is not the primary address of "+
3530 "the target") % addr
3531 epat.delete()
3532 # All OK, let's nuke it all.
3533 result = []
3534 ea = Email.EmailAddress(self.db)
3535 for r in et.get_addresses():
3536 ea.clear()
3537 ea.find(r['address_id'])
3538 result.append({'address': self._get_address(ea)})
3539 ea.delete()
3540 return result
3541
3542 _rt_pipe = ("|/local/bin/rt-mailgate --action %(action)s --queue %(queue)s "
3543 "--url https://%(host)s/")
3544 # This assumes that the only RE meta character in _rt_pipe is the
3545 # leading pipe.
3546 _rt_patt = "^\\" + _rt_pipe % {'action': '(\S+)',
3547 'queue': '(\S+)',
3548 'host': '(\S+)'} + "$"
3549
3550 # email rt_create queue[@host] address [force]
3551 all_commands['email_rt_create'] = Command(
3552 ("email", "rt_create"),
3553 RTQueue(), EmailAddress(),
3554 YesNo(help_ref="yes_no_force", optional=True),
3555 perm_filter='can_rt_create')
3556 def email_rt_create(self, operator, queuename, addr, force="No"):
3557 queue, host = self._resolve_rt_name(queuename)
3558 rt_dom = self._get_email_domain(host)
3559 op = operator.get_entity_id()
3560 self.ba.can_rt_create(op, domain=rt_dom)
3561 try:
3562 self._get_rt_email_target(queue, host)
3563 except CerebrumError:
3564 pass
3565 else:
3566 raise CerebrumError, "RT queue %s already exists" % queuename
3567 addr_lp, addr_domain_name = self._split_email_address(addr)
3568 addr_dom = self._get_email_domain(addr_domain_name)
3569 if addr_domain_name != host:
3570 self.ba.can_email_address_add(operator.get_entity_id(),
3571 domain=addr_dom)
3572 replaced_lists = []
3573
3574 # Unusual characters will raise an exception, a too short name
3575 # will return False, which we ignore for the queue name.
3576 self._is_ok_mailing_list_name(queue)
3577
3578 # The submission address is only allowed to be short if it is
3579 # equal to the queue name, or the operator is a global
3580 # postmaster.
3581 if not (self._is_ok_mailing_list_name(addr_lp) or
3582 addr == queue + "@" + host or
3583 self.ba.is_postmaster(op)):
3584 raise CerebrumError, "Illegal address for submission: %s" % addr
3585 try:
3586 et, ea = self._get_email_target_and_address(addr)
3587 except CerebrumError:
3588 pass
3589 else:
3590 raise CerebrumError, "Address <%s> is in use" % addr
3591 acc = self._get_account("exim")
3592 et = Email.EmailTarget(self.db)
3593 ea = Email.EmailAddress(self.db)
3594 cmd = self._rt_pipe % {'action': "correspond",
3595 'queue': queue, 'host': host}
3596 et.populate(self.const.email_target_RT, alias=cmd,
3597 using_uid=acc.entity_id)
3598 et.write_db()
3599 # Add primary address
3600 ea.populate(addr_lp, addr_dom.entity_id, et.entity_id)
3601 ea.write_db()
3602 epat = Email.EmailPrimaryAddressTarget(self.db)
3603 epat.populate(ea.entity_id, parent=et)
3604 epat.write_db()
3605 for alias in replaced_lists:
3606 if alias == addr:
3607 continue
3608 lp, dom = self._split_email_address(alias)
3609 alias_dom = self._get_email_domain(dom)
3610 ea.clear()
3611 ea.populate(lp, alias_dom.entity_id, et.entity_id)
3612 ea.write_db()
3613 # Add RT internal address
3614 if addr_lp != queue or addr_domain_name != host:
3615 ea.clear()
3616 ea.populate(queue, rt_dom.entity_id, et.entity_id)
3617 ea.write_db()
3618
3619 # Moving on to the comment address
3620 et.clear()
3621 cmd = self._rt_pipe % {'queue': queue, 'action': "comment",
3622 'host': host}
3623 et.populate(self.const.email_target_RT, alias=cmd,
3624 using_uid=acc.entity_id)
3625 et.write_db()
3626 ea.clear()
3627 ea.populate("%s-comment" % queue, rt_dom.entity_id,
3628 et.entity_id)
3629 ea.write_db()
3630 msg = "RT queue %s on %s added" % (queue, host)
3631 if replaced_lists:
3632 msg += ", replacing mailing list(s) %s" % ", ".join(replaced_lists)
3633 addr = queue + "@" + host
3634 self._register_spam_settings(addr, self.const.email_target_RT)
3635 self._register_filter_settings(addr, self.const.email_target_RT)
3636 return msg
3637
3638 # email rt_delete queue[@host]
3639 all_commands['email_rt_delete'] = Command(
3640 ("email", "rt_delete"),
3641 EmailAddress(),
3642 fs=FormatSuggestion([("Deleted address: %s", ("address", ))]),
3643 perm_filter='can_rt_delete')
3644 def email_rt_delete(self, operator, queuename):
3645 queue, host = self._resolve_rt_name(queuename)
3646 rt_dom = self._get_email_domain(host)
3647 self.ba.can_rt_delete(operator.get_entity_id(), domain=rt_dom)
3648 et = Email.EmailTarget(self.db)
3649 ea = Email.EmailAddress(self.db)
3650 epat = Email.EmailPrimaryAddressTarget(self.db)
3651 result = []
3652
3653 for target_id in self._get_all_related_rt_targets(queuename):
3654 try:
3655 et.clear()
3656 et.find(target_id)
3657 except Errors.NotFoundError:
3658 continue
3659
3660 epat.clear()
3661 try:
3662 epat.find(et.entity_id)
3663 except Errors.NotFoundError:
3664 pass
3665 else:
3666 epat.delete()
3667 for r in et.get_addresses():
3668 addr = '%(local_part)s@%(domain)s' % r
3669 ea.clear()
3670 ea.find_by_address(addr)
3671 ea.delete()
3672 result.append({'address': addr})
3673 et.delete()
3674
3675 return result
3676
3677 # email rt_add_address queue[@host] address
3678 all_commands['email_rt_add_address'] = Command(
3679 ('email', 'rt_add_address'),
3680 RTQueue(), EmailAddress(),
3681 perm_filter='can_rt_address_add')
3682 def email_rt_add_address(self, operator, queuename, address):
3683 queue, host = self._resolve_rt_name(queuename)
3684 rt_dom = self._get_email_domain(host)
3685 self.ba.can_rt_address_add(operator.get_entity_id(), domain=rt_dom)
3686 et = self._get_rt_email_target(queue, host)
3687 lp, dom = self._split_email_address(address)
3688 ed = self._get_email_domain(dom)
3689 if host != dom:
3690 self.ba.can_email_address_add(operator.get_entity_id(),
3691 domain=ed)
3692 ea = Email.EmailAddress(self.db)
3693 try:
3694 ea.find_by_local_part_and_domain(lp, ed.entity_id)
3695 raise CerebrumError, "Address already exists (%s)" % address
3696 except Errors.NotFoundError:
3697 pass
3698 if not (self._is_ok_mailing_list_name(lp) or
3699 self.ba.is_postmaster(operator.get_entity_id())):
3700 raise CerebrumError, "Illegal queue address: %s" % address
3701 ea.clear()
3702 ea.populate(lp, ed.entity_id, et.entity_id)
3703 ea.write_db()
3704 return ("OK, added '%s' as e-mail address for '%s'" %
3705 (address, queuename))
3706
3707 # email rt_remove_address queue address
3708 all_commands['email_rt_remove_address'] = Command(
3709 ('email', 'rt_remove_address'),
3710 RTQueue(), EmailAddress(),
3711 perm_filter='can_email_address_delete')
3712 def email_rt_remove_address(self, operator, queuename, address):
3713 queue, host = self._resolve_rt_name(queuename)
3714 rt_dom = self._get_email_domain(host)
3715 self.ba.can_rt_address_remove(operator.get_entity_id(), domain=rt_dom)
3716 et = self._get_rt_email_target(queue, host)
3717 return self._remove_email_address(et, address)
3718
3719 # email rt_primary_address address
3720 all_commands['email_rt_primary_address'] = Command(
3721 ("email", "rt_primary_address"),
3722 RTQueue(), EmailAddress(),
3723 fs=FormatSuggestion([("New primary address: '%s'", ("address", ))]),
3724 perm_filter="can_rt_address_add")
3725 def email_rt_primary_address(self, operator, queuename, address):
3726 queue, host = self._resolve_rt_name(queuename)
3727 self.ba.can_rt_address_add(operator.get_entity_id(),
3728 domain=self._get_email_domain(host))
3729 rt = self._get_rt_email_target(queue, host)
3730 et, ea = self._get_email_target_and_address(address)
3731 if rt.entity_id != et.entity_id:
3732 raise CerebrumError, \
3733 ("Address <%s> is not associated with RT queue %s" %
3734 (address, queuename))
3735 return self._set_email_primary_address(et, ea, address)
3736
3737 def _resolve_rt_name(self, queuename):
3738 """Return queue and host of RT queue as tuple."""
3739 if queuename.count('@') == 0:
3740 # Use the default host
3741 return queuename, "rt.uit.no"
3742 elif queuename.count('@') > 1:
3743 raise CerebrumError, "Invalid RT queue name: %s" % queuename
3744 return queuename.split('@')
3745
3746 def _get_all_related_rt_targets(self, address):
3747 """This method locates and returns all ETs associated with the same RT
3748 queue.
3749
3750 Given any address associated with a RT queue, this method returns
3751 all the ETs associated with that RT queue. E.g.: 'foo@domain' will return
3752 'foo@domain' and 'foo-comment@queuehost'
3753
3754 If address (EA) is not associated with a RT queue, this method
3755 raises an exception. Otherwise a list of ET entity_ids is returned.
3756
3757 @type address: basestring
3758 @param address:
3759 One of the mail addresses associated with a RT queue.
3760
3761 @rtype: sequence (of ints)
3762 @return:
3763 A sequence with entity_ids of all ETs related to the RT queue that address
3764 is related to.
3765
3766 """
3767
3768 et = Email.EmailTarget(self.db)
3769 queue, host = self._get_rt_queue_and_host(address)
3770 targets = set([])
3771 for action in ("correspond", "comment"):
3772 alias = self._rt_pipe % { 'action': action, 'queue': queue,
3773 'host': host }
3774 try:
3775 et.clear()
3776 et.find_by_alias(alias)
3777 except Errors.NotFoundError:
3778 continue
3779
3780 targets.add(et.entity_id)
3781
3782 if not targets:
3783 raise CerebrumError, ("RT queue %s on host %s not found" %
3784 (queue, host))
3785
3786 return targets
3787
3788 # end _get_all_related_rt_targets
3789
3790 def _get_rt_email_target(self, queue, host):
3791 et = Email.EmailTarget(self.db)
3792 try:
3793 et.find_by_alias(self._rt_pipe % { 'action': "correspond",
3794 'queue': queue, 'host': host })
3795 except Errors.NotFoundError:
3796 raise CerebrumError, ("Unknown RT queue %s on host %s" %
3797 (queue, host))
3798 return et
3799
3800 def _get_rt_queue_and_host(self, address):
3801 et, addr = self._get_email_target_and_address(address)
3802
3803 try:
3804 m = re.match(self._rt_patt, et.get_alias())
3805 return m.group(2), m.group(3)
3806 except AttributeError:
3807 raise CerebrumError("Could not get queue and host for %s" % address)
3808
3809 # email migrate
3810 all_commands['email_migrate'] = Command(
3811 ("email", "migrate"),
3812 AccountName(help_ref="account_name", repeat=True),
3813 perm_filter='can_email_migrate')
3814 def email_migrate(self, operator, uname):
3815 acc = self._get_account(uname)
3816 op = operator.get_entity_id()
3817 self.ba.can_email_migrate(op, acc)
3818 for r in acc.get_spread():
3819 if r['spread'] == int(self.const.spread_uit_imap):
3820 raise CerebrumError, "%s is already an IMAP user" % uname
3821 acc.add_spread(self.const.spread_uit_imap)
3822 if op != acc.entity_id:
3823 # the local sysadmin should get a report as well, if
3824 # possible, so change the request add_spread() put in so
3825 # that he is named as the requestee. the list of requests
3826 # may turn out to be empty, ie. processed already, but this
3827 # unlikely race condition is too hard to fix.
3828 br = BofhdRequests(self.db, self.const)
3829 for r in br.get_requests(operation=self.const.bofh_email_move,
3830 entity_id=acc.entity_id):
3831 br.delete_request(request_id=r['request_id'])
3832 br.add_request(op, r['run_at'], r['operation'], r['entity_id'],
3833 r['destination_id'], r['state_data'])
3834 return 'OK'
3835
3836 # email move
3837 all_commands['email_move'] = Command(
3838 ("email", "move"),
3839 AccountName(help_ref="account_name", repeat=True),
3840 SimpleString(help_ref='string_email_host'),
3841 SimpleString(help_ref='string_email_move_type', optional=True),
3842 Date(optional=True),
3843 perm_filter='can_email_move')
3844 def email_move(self, operator, uname, server, move_type='file', when=None):
3845 acc = self._get_account(uname)
3846 self.ba.can_email_move(operator.get_entity_id(), acc)
3847 et = Email.EmailTarget(self.db)
3848 et.find_by_target_entity(acc.entity_id)
3849 old_server = et.email_server_id
3850 es = Email.EmailServer(self.db)
3851 try:
3852 es.find_by_name(server)
3853 except Errors.NotFoundError:
3854 raise CerebrumError, ("%s is not registered as an e-mail server") % server
3855 if old_server == es.entity_id:
3856 raise CerebrumError, "User is already at %s" % server
3857
3858 # Explicitly check if move_type is 'file' or 'nofile'. Abort if it isn't
3859 if move_type == 'nofile':
3860 et.email_server_id = es.entity_id
3861 et.write_db()
3862 return "OK, updated e-mail server for %s (to %s)" % (uname, server)
3863 elif not move_type == 'file':
3864 raise CerebrumError, ("Unknown move_type '%s'; must be "
3865 "either 'file' or 'nofile'" % move_type)
3866
3867 # TODO: Remove this when code has been checked after migrating to
3868 # murder.
3869 raise CerebrumError("Only 'nofile' is to be used at this time.")
3870
3871 if when is None:
3872 when = DateTime.now()
3873 else:
3874 when = self._parse_date(when)
3875 if when < DateTime.now():
3876 raise CerebrumError("Request time must be in the future")
3877
3878 if es.email_server_type == self.const.email_server_type_cyrus:
3879 spreads = [int(r['spread']) for r in acc.get_spread()]
3880 br = BofhdRequests(self.db, self.const)
3881 if not self.const.spread_uit_imap in spreads:
3882 # UiO's add_spread mixin will not do much since
3883 # email_server_id is set to a Cyrus server already.
3884 acc.add_spread(self.const.spread_uit_imap)
3885 # Create the mailbox.
3886 req = br.add_request(operator.get_entity_id(), when,
3887 self.const.bofh_email_create,
3888 acc.entity_id, es.entity_id)
3889 # Now add a move request.
3890 br.add_request(operator.get_entity_id(), when,
3891 self.const.bofh_email_move,
3892 acc.entity_id, es.entity_id, state_data=req)
3893 # Norwegian (nynorsk) names:
3894 wdays_nn = ["mandag", "tysdag", "onsdag", "torsdag",
3895 "fredag", "laurdag", "søndag"]
3896 when_nn = "%s %d. kl %02d:%02d" % \
3897 (wdays_nn[when.day_of_week],
3898 when.day, when.hour, when.minute - when.minute % 10)
3899 nth_en = ["th"] * 32
3900 nth_en[1] = nth_en[21] = nth_en[31] = "st"
3901 nth_en[2] = nth_en[22] = "nd"
3902 nth_en[3] = nth_en[23] = "rd"
3903 when_en = "%s %d%s at %02d:%02d" % \
3904 (DateTime.Weekday[when.day_of_week],
3905 when.day, nth_en[when.day],
3906 when.hour, when.minute - when.minute % 10)
3907 try:
3908 Utils.mail_template(acc.get_primary_mailaddress(),
3909 cereconf.USER_EMAIL_MOVE_WARNING,
3910 sender="bas-admin@cc.uit.no",
3911 substitute={'USER': acc.account_name,
3912 'WHEN_EN': when_en,
3913 'WHEN_NN': when_nn})
3914 except Exception, e:
3915 self.logger.info("Sending mail failed: %s", e)
3916 else:
3917 # TBD: should we remove spread_uio_imap ?
3918 # It does not do much good to add to a bofh request, mvmail
3919 # can't handle this anyway.
3920 raise CerebrumError, "can't move to non-IMAP server"
3921 return "OK, '%s' scheduled for move to '%s'" % (uname, server)
3922
3923 # email pause
3924 all_commands['email_pause'] = Command(
3925 ("email", "pause"),
3926 SimpleString(help_ref='string_email_on_off'),
3927 AccountName(help_ref="account_name"),
3928 perm_filter='can_email_pause')
3929 def email_pause(self, operator, on_off, uname):
3930 et, acc = self._get_email_target_and_account(uname)
3931
3932 # exchange-relatert-jazz
3933 # there is no point in registering mailPause for
3934 # Exchange mailboxes
3935 #if acc.has_spread(self.const.spread_exchange_account):
3936 # return "Modifying mailPause for Exchange-mailboxes is not allowed!"
3937
3938 self.ba.can_email_pause(operator.get_entity_id(), acc)
3939 self._ldap_init()
3940
3941 dn = cereconf.LDAP_EMAIL_DN % et.entity_id
3942
3943 if on_off in ('ON', 'on'):
3944 et.populate_trait(self.const.trait_email_pause, et.entity_id)
3945 et.write_db()
3946 r = self._ldap_modify(dn, "mailPause", "TRUE")
3947 if r:
3948 et.commit()
3949 return "mailPause set for '%s'" % uname
3950 else:
3951 et._db.rollback()
3952 return "Error: mailPause not set for '%s'" % uname
3953
3954 elif on_off in ('OFF', 'off'):
3955 try:
3956 et.delete_trait(self.const.trait_email_pause)
3957 et.write_db()
3958 except Errors.NotFoundError:
3959 return "Error: mailPause not unset for '%s'" % uname
3960
3961 r = self._ldap_modify(dn, "mailPause")
3962 if r:
3963 et.commit()
3964 return "mailPause unset for '%s'" % uname
3965 else:
3966 et._db.rollback()
3967 return "Error: mailPause not unset for '%s'" % uname
3968
3969 else:
3970 raise CerebrumError, ('Mailpause is either \'ON\' or \'OFF\'')
3971
3972 # email pause list
3973 all_commands['email_list_pause'] = Command(
3974 ("email", "list_pause"),
3975 perm_filter='can_email_pause',
3976 fs=FormatSuggestion([("Paused addresses:\n%s", ("paused", ))]),)
3977 def email_list_pause(self, operator):
3978 self.ba.can_email_pause(operator.get_entity_id())
3979 ac = self.Account_class(self.db)
3980 et = Email.EmailTarget(self.db)
3981 ea = Email.EmailAddress(self.db)
3982 epa = Email.EmailPrimaryAddressTarget(self.db)
3983
3984 res = []
3985 for row in et.list_traits(code=self.const.trait_email_pause):
3986 et.clear()
3987 et.find(row['entity_id'])
3988 if self.const.EmailTarget(et.email_target_type) == \
3989 self.const.email_target_account:
3990 ac.clear()
3991 ac.find(et.email_target_entity_id)
3992 res.append(ac.account_name)
3993 else:
3994 epa.clear()
3995 epa.find_by_alias(et.email_target_alias)
3996 ea.clear()
3997 ea.find(epa.email_primaddr_id)
3998 res.append(ea.get_address())
3999
4000 return {'paused': '\n'.join(res)}
4001
4002 # email quota <uname>+ hardquota-in-mebibytes [softquota-in-percent]
4003 all_commands['email_quota'] = Command(
4004 ('email', 'quota'),
4005 AccountName(help_ref='account_name', repeat=True),
4006 Integer(help_ref='number_size_mib'),
4007 Integer(help_ref='number_percent', optional=True),
4008 perm_filter='can_email_set_quota')
4009 def email_quota(self, operator, uname, hquota,
4010 squota=cereconf.EMAIL_SOFT_QUOTA):
4011 acc = self._get_account(uname)
4012 op = operator.get_entity_id()
4013 self.ba.can_email_set_quota(op, acc)
4014 if not str(hquota).isdigit() or not str(squota).isdigit():
4015 raise CerebrumError, "Quota must be numeric"
4016 hquota = int(hquota)
4017 squota = int(squota)
4018 if hquota < 100 and hquota != 0:
4019 raise CerebrumError, "The hard quota can't be less than 100 MiB"
4020 if hquota > 1024*1024:
4021 raise CerebrumError, "The hard quota can't be more than 1 TiB"
4022 if squota < 10 or squota > 99:
4023 raise CerebrumError, ("The soft quota must be in the interval "+
4024 "10% to 99%")
4025 et = Email.EmailTarget(self.db)
4026 try:
4027 et.find_by_target_entity(acc.entity_id)
4028 except Errors.NotFoundError:
4029 raise CerebrumError, ("The account %s has no e-mail data "+
4030 "associated with it") % uname
4031 eq = Email.EmailQuota(self.db)
4032 change = False
4033 try:
4034 eq.find_by_target_entity(acc.entity_id)
4035 if eq.email_quota_hard != hquota:
4036 change = True
4037 eq.email_quota_hard = hquota
4038 eq.email_quota_soft = squota
4039 except Errors.NotFoundError:
4040 eq.clear()
4041 if hquota != 0:
4042 eq.populate(squota, hquota, parent=et)
4043 change = True
4044 if hquota == 0:
4045 eq.delete()
4046 else:
4047 eq.write_db()
4048 if change:
4049 # If we're supposed to put a request in BofhdRequests we'll have to
4050 # be sure that the user getting the quota is a Cyrus-user. If not,
4051 # Cyrus will spew out errors telling us "user foo is not a cyrus-user".
4052 if not et.email_server_id:
4053 raise CerebrumError, ("The account %s has no e-mail server "+
4054 "associated with it") % uname
4055 es = Email.EmailServer(self.db)
4056 es.find(et.email_server_id)
4057
4058 if es.email_server_type == self.const.email_server_type_cyrus:
4059 br = BofhdRequests(self.db, self.const)
4060 # if this operator has already asked for a quota change, but
4061 # process_bofh_requests hasn't run yet, delete the existing
4062 # request to avoid the annoying error message.
4063 for r in br.get_requests(operation=self.const.bofh_email_hquota,
4064 operator_id=op, entity_id=acc.entity_id):
4065 br.delete_request(request_id=r['request_id'])
4066 br.add_request(op, br.now, self.const.bofh_email_hquota,
4067 acc.entity_id, None)
4068 return "OK, set quota for '%s'" % uname
4069
4070 # email add_filter filter address
4071 all_commands['email_add_filter'] = Command(
4072 ('email', 'add_filter'),
4073 SimpleString(help_ref='string_email_filter'),
4074 SimpleString(help_ref='string_email_target_name', repeat="True"),
4075 perm_filter='can_email_spam_settings') # _is_local_postmaster')
4076
4077 def email_add_filter(self, operator, filter, address):
4078 """Add a filter to an existing e-mail target."""
4079 et, acc = self._get_email_target_and_account(address)
4080 self.ba.can_email_spam_settings(operator.get_entity_id(),
4081 acc, et)
4082 etf = Email.EmailTargetFilter(self.db)
4083 filter_code = self._get_constant(self.const.EmailTargetFilter, filter)
4084
4085 target_ids = [et.entity_id]
4086 if et.email_target_type == self.const.email_target_Sympa:
4087 # The only way we can get here is if uname is actually an e-mail
4088 # address on its own.
4089 target_ids = self._get_all_related_maillist_targets(address)
4090 elif et.email_target_type == self.const.email_target_RT:
4091 target_ids = self._get_all_related_rt_targets(address)
4092 for target_id in target_ids:
4093 try:
4094 et.clear()
4095 et.find(target_id)
4096 except Errors.NotFoundError:
4097 continue
4098
4099 try:
4100 etf.clear()
4101 etf.find(et.entity_id, filter_code)
4102 except Errors.NotFoundError:
4103 etf.clear()
4104 etf.populate(filter_code, parent=et)
4105 etf.write_db()
4106 return "Ok, registered filter %s for %s" % (filter, address)
4107
4108 # email remove_filter filter address
4109 all_commands['email_remove_filter'] = Command(
4110 ('email', 'remove_filter'),
4111 SimpleString(help_ref='string_email_filter'),
4112 SimpleString(help_ref='string_email_target_name', repeat="True"),
4113 perm_filter='can_email_spam_settings') # _is_local_postmaster')
4114
4115 def email_remove_filter(self, operator, filter, address):
4116 """Remove email fitler for account."""
4117 et, acc = self._get_email_target_and_account(address)
4118 self.ba.can_email_spam_settings(operator.get_entity_id(),
4119 acc, et)
4120
4121 etf = Email.EmailTargetFilter(self.db)
4122 filter_code = self._get_constant(self.const.EmailTargetFilter, filter)
4123 target_ids = [et.entity_id]
4124 if et.email_target_type == self.const.email_target_Sympa:
4125 # The only way we can get here is if uname is actually an e-mail
4126 # address on its own.
4127 target_ids = self._get_all_related_maillist_targets(address)
4128 elif et.email_target_type == self.const.email_target_RT:
4129 target_ids = self._get_all_related_rt_targets(address)
4130 processed = list()
4131 for target_id in target_ids:
4132 try:
4133 etf.clear()
4134 etf.find(target_id, filter_code)
4135 etf.disable_email_target_filter(filter_code)
4136 etf.write_db()
4137 processed.append(target_id)
4138 except Errors.NotFoundError:
4139 pass
4140
4141 if not processed:
4142 raise CerebrumError("Could not find any filters %s for address %s "
4143 "(or any related targets)" % (filter, address))
4144
4145 return "Ok, removed filter %s for %s" % (filter, address)
4146
4147 # email spam_level <level> <name>+
4148 # exchange-relatert-jazz
4149 # made it possible to use this cmd for adding spam_level
4150 # to dist group targets
4151 all_commands['email_spam_level'] = Command(
4152 ('email', 'spam_level'),
4153 SimpleString(help_ref='spam_level'),
4154 SimpleString(help_ref="dlgroup_or_account_name", repeat=True),
4155 perm_filter='can_email_spam_settings')
4156 def email_spam_level(self, operator, level, name):
4157 """Set the spam level for the EmailTarget associated with username.
4158 It is also possible for super users to pass the name of other email
4159 targets."""
4160 try:
4161 levelcode = int(self.const.EmailSpamLevel(level))
4162 except Errors.NotFoundError:
4163 raise CerebrumError("Spam level code not found: {}".format(level))
4164 try:
4165 et, acc = self._get_email_target_and_account(name)
4166 except CerebrumError, e:
4167 # check if a distribution-group with an appropriate target
4168 # is registered by this name
4169 try:
4170 et, grp = self._get_email_target_and_dlgroup(name)
4171 except CerebrumError, e:
4172 raise e
4173 self.ba.can_email_spam_settings(operator.get_entity_id(),
4174 acc, et) or \
4175 self.ba.is_postmaster(operator.get_entity_id())
4176 esf = Email.EmailSpamFilter(self.db)
4177 # All this magic with target ids is necessary to accomodate MLs (all
4178 # ETs "related" to the same ML should have the
4179 # spam settings should be processed )
4180 target_ids = [et.entity_id]
4181 # The only way we can get here is if uname is actually an e-mail
4182 # address on its own.
4183 if et.email_target_type == self.const.email_target_Sympa:
4184 target_ids = self._get_all_related_maillist_targets(name)
4185 elif et.email_target_type == self.const.email_target_RT:
4186 targets_ids = self._get_all_related_rt_targets(name)
4187
4188 for target_id in target_ids:
4189 try:
4190 et.clear()
4191 et.find(target_id)
4192 except Errors.NotFoundError:
4193 continue
4194 try:
4195 esf.clear()
4196 esf.find(et.entity_id)
4197 esf.email_spam_level = levelcode
4198 except Errors.NotFoundError:
4199 esf.clear()
4200 esf.populate(levelcode, self.const.email_spam_action_none,
4201 parent=et)
4202 esf.write_db()
4203
4204 return "OK, set spam-level for '%s'" % name
4205
4206 # email spam_action <action> <uname>+
4207 all_commands['email_spam_action'] = Command(
4208 ('email', 'spam_action'),
4209 SimpleString(help_ref='spam_action'),
4210 SimpleString(help_ref="dlgroup_or_account_name", repeat=True),
4211 perm_filter='can_email_spam_settings')
4212 def email_spam_action(self, operator, action, name):
4213 """Set the spam action for the EmailTarget associated with username.
4214 It is also possible for super users to pass the name of other email
4215 targets."""
4216 try:
4217 actioncode = int(self.const.EmailSpamAction(action))
4218 except Errors.NotFoundError:
4219 raise CerebrumError(
4220 "Spam action code not found: {}".format(action))
4221 try:
4222 et, acc = self._get_email_target_and_account(name)
4223 except CerebrumError, e:
4224 # check if a distribution-group with an appropriate target
4225 # is registered by this name
4226 try:
4227 et, grp = self._get_email_target_and_dlgroup(name)
4228 except CerebrumError, e:
4229 raise e
4230 self.ba.can_email_spam_settings(operator.get_entity_id(),
4231 acc, et) or \
4232 self.ba.is_postmaster(operator.get_entity_id())
4233 esf = Email.EmailSpamFilter(self.db)
4234 # All this magic with target ids is necessary to accomodate MLs (all
4235 # ETs "related" to the same ML should have the
4236 # spam settings should be processed )
4237 target_ids = [et.entity_id]
4238 # The only way we can get here is if uname is actually an e-mail
4239 # address on its own.
4240 if et.email_target_type == self.const.email_target_Sympa:
4241 target_ids = self._get_all_related_maillist_targets(name)
4242 elif et.email_target_type == self.const.email_target_RT:
4243 target_ids = self._get_all_related_rt_targets(name)
4244
4245 for target_id in target_ids:
4246 try:
4247 et.clear()
4248 et.find(target_id)
4249 except Errors.NotFoundError:
4250 continue
4251
4252 try:
4253 esf.clear()
4254 esf.find(et.entity_id)
4255 esf.email_spam_action = actioncode
4256 except Errors.NotFoundError:
4257 esf.clear()
4258 esf.populate(self.const.email_spam_level_none, actioncode,
4259 parent=et)
4260 esf.write_db()
4261
4262 return "OK, set spam-action for '%s'" % name
4263
4264 # email tripnote on|off <uname> [<begin-date>]
4265 all_commands['email_tripnote'] = Command(
4266 ('email', 'tripnote'),
4267 SimpleString(help_ref='email_tripnote_action'),
4268 AccountName(help_ref='account_name'),
4269 SimpleString(help_ref='date', optional=True),
4270 perm_filter='can_email_tripnote_toggle')
4271 def email_tripnote(self, operator, action, uname, when=None):
4272 if action == 'on':
4273 enable = True
4274 elif action == 'off':
4275 enable = False
4276 else:
4277 raise CerebrumError, ("Unknown tripnote action '%s', choose one "+
4278 "of on or off") % action
4279 acc = self._get_account(uname)
4280 # exchange-relatert-jazz
4281 # For Exchange-mailboxes vacation must be registered via
4282 # Outlook/OWA since smart host solution for Exchange@UiO
4283 # could not be implemented. When migration to Exchange
4284 # is completed this method should be changed and adding
4285 # vacation for any account disallowed. Jazz (2013-11)
4286 if acc.has_spread(self.const.spread_exchange_account):
4287 return "Sorry, Exchange-users must enable vacation messages via OWA!"
4288 self.ba.can_email_tripnote_toggle(operator.get_entity_id(), acc)
4289 ev = Email.EmailVacation(self.db)
4290 ev.find_by_target_entity(acc.entity_id)
4291 # TODO: If 'enable' at this point actually is None (which, by
4292 # the looks of the if-else clause at the top seems
4293 # impossible), opposite_status won't be defined, and hence the
4294 # ._find_tripnote() call below will fail.
4295 if enable is not None:
4296 opposite_status = not enable
4297 date = self._find_tripnote(uname, ev, when, opposite_status)
4298 ev.enable_vacation(date, enable)
4299 ev.write_db()
4300 return "OK, set tripnote to '%s' for '%s'" % (action, uname)
4301
4302 all_commands['email_list_tripnotes'] = Command(
4303 ('email', 'list_tripnotes'),
4304 AccountName(help_ref='account_name'),
4305 perm_filter='can_email_tripnote_toggle',
4306 fs=FormatSuggestion([
4307 ('%s%s -- %s: %s\n%s\n',
4308 ("dummy", format_day('start_date'), format_day('end_date'),
4309 "enable", "text"))]))
4310 def email_list_tripnotes(self, operator, uname):
4311 acc = self._get_account(uname)
4312 self.ba.can_email_tripnote_toggle(operator.get_entity_id(), acc)
4313 try:
4314 self.ba.can_email_tripnote_edit(operator.get_entity_id(), acc)
4315 hide = False
4316 except:
4317 hide = True
4318 ev = Email.EmailVacation(self.db)
4319 try:
4320 ev.find_by_target_entity(acc.entity_id)
4321 except Errors.NotFoundError:
4322 return "No tripnotes for %s" % uname
4323 now = self._today()
4324 act_date = None
4325 for r in ev.get_vacation():
4326 if r['end_date'] is not None and r['start_date'] > r['end_date']:
4327 self.logger.info(
4328 "bogus tripnote for %s, start at %s, end at %s"
4329 % (uname, r['start_date'], r['end_date']))
4330 ev.delete_vacation(r['start_date'])
4331 ev.write_db()
4332 continue
4333 if r['enable'] == 'F':
4334 continue
4335 if r['end_date'] is not None and r['end_date'] < now:
4336 continue
4337 if r['start_date'] > now:
4338 break
4339 # get_vacation() returns a list ordered by start_date, so
4340 # we know this one is newer.
4341 act_date = r['start_date']
4342 result = []
4343 for r in ev.get_vacation():
4344 text = r['vacation_text']
4345 if r['enable'] == 'F':
4346 enable = "OFF"
4347 elif r['end_date'] is not None and r['end_date'] < now:
4348 enable = "OLD"
4349 elif r['start_date'] > now:
4350 enable = "PENDING"
4351 else:
4352 enable = "ON"
4353 if act_date is not None and r['start_date'] == act_date:
4354 enable = "ACTIVE"
4355 elif hide:
4356 text = "<text is hidden>"
4357 # TODO: FormatSuggestion won't work with a format_day()
4358 # coming first, so we use an empty dummy string as a
4359 # workaround.
4360 result.append({'dummy': "",
4361 'start_date': r['start_date'],
4362 'end_date': r['end_date'],
4363 'enable': enable,
4364 'text': text})
4365 if result:
4366 return result
4367 else:
4368 return "No tripnotes for %s" % uname
4369
4370 # email add_tripnote <uname> <text> <begin-date>[-<end-date>]
4371 all_commands['email_add_tripnote'] = Command(
4372 ('email', 'add_tripnote'),
4373 AccountName(help_ref='account_name'),
4374 SimpleString(help_ref='tripnote_text'),
4375 SimpleString(help_ref='string_from_to'),
4376 perm_filter='can_email_tripnote_edit')
4377 def email_add_tripnote(self, operator, uname, text, when=None):
4378 acc = self._get_account(uname)
4379 # exchange-relatert-jazz
4380 # For Exchange-mailboxes vacation must be registered via
4381 # OWA since smart host solution for Exchange@UiO
4382 # could not be implemented. When migration to Exchange
4383 # is completed this method should be changed and adding
4384 # vacation for any account disallowed. Jazz (2013-11)
4385 if acc.has_spread(self.const.spread_exchange_account):
4386 return "Sorry, Exchange-users must add vacation messages via OWA!"
4387 self.ba.can_email_tripnote_edit(operator.get_entity_id(), acc)
4388 date_start, date_end = self._parse_date_from_to(when)
4389 now = self._today()
4390 if date_end is not None and date_end < now:
4391 raise CerebrumError, "Won't add already obsolete tripnotes"
4392 ev = Email.EmailVacation(self.db)
4393 ev.find_by_target_entity(acc.entity_id)
4394 for v in ev.get_vacation():
4395 if date_start is not None and v['start_date'] == date_start:
4396 raise CerebrumError, ("There's a tripnote starting on %s "+
4397 "already") % str(date_start)[:10]
4398
4399 # FIXME: The SquirrelMail plugin sends CR LF which xmlrpclib
4400 # (AFAICT) converts into LF LF. Remove the double line
4401 # distance. jbofh users have to send backslash n anyway, so
4402 # this won't affect common usage.
4403 text = text.replace('\n\n', '\n')
4404 text = text.replace('\\n', '\n')
4405 ev.add_vacation(date_start, text, date_end, enable=True)
4406 ev.write_db()
4407 return "OK, added tripnote for '%s'" % uname
4408
4409 # email remove_tripnote <uname> [<when>]
4410 all_commands['email_remove_tripnote'] = Command(
4411 ('email', 'remove_tripnote'),
4412 AccountName(help_ref='account_name'),
4413 SimpleString(help_ref='date', optional=True),
4414 perm_filter='can_email_tripnote_edit')
4415 def email_remove_tripnote(self, operator, uname, when=None):
4416 acc = self._get_account(uname)
4417 self.ba.can_email_tripnote_edit(operator.get_entity_id(), acc)
4418 # TBD: This variable isn't used; is this call a sign of rot,
4419 # or is it here for input validation?
4420 start = self._parse_date(when)
4421 ev = Email.EmailVacation(self.db)
4422 ev.find_by_target_entity(acc.entity_id)
4423 date = self._find_tripnote(uname, ev, when)
4424 ev.delete_vacation(date)
4425 ev.write_db()
4426 return "OK, removed tripnote for '%s'" % uname
4427
4428 def _find_tripnote(self, uname, ev, when=None, enabled=None):
4429 vacs = ev.get_vacation()
4430 if enabled is not None:
4431 nv = []
4432 for v in vacs:
4433 if (v['enable'] == 'T') == enabled:
4434 nv.append(v)
4435 vacs = nv
4436 if len(vacs) == 0:
4437 if enabled is None:
4438 raise CerebrumError, "User %s has no stored tripnotes" % uname
4439 elif enabled:
4440 raise CerebrumError, "User %s has no enabled tripnotes" % uname
4441 else:
4442 raise CerebrumError, "User %s has no disabled tripnotes" % uname
4443 elif len(vacs) == 1:
4444 return vacs[0]['start_date']
4445 elif when is None:
4446 raise CerebrumError, ("User %s has more than one tripnote, "+
4447 "specify which one by adding the "+
4448 "start date to command") % uname
4449 start = self._parse_date(when)
4450 best = None
4451 for r in vacs:
4452 delta = abs (r['start_date'] - start)
4453 if best is None or delta < best_delta:
4454 best = r['start_date']
4455 best_delta = delta
4456 # TODO: in PgSQL, date arithmetic is in days, but casting
4457 # it to int returns seconds. The behaviour is undefined
4458 # in the DB-API.
4459 if abs(int(best_delta)) > 1.5*86400:
4460 raise CerebrumError, ("There are no tripnotes starting "+
4461 "at %s") % when
4462 return best
4463
4464 # email update <uname>
4465 # Anyone can run this command. Ideally, it should be a no-op,
4466 # and we should remove it when that is true.
4467 all_commands['email_update'] = Command(
4468 ('email', 'update'),
4469 AccountName(help_ref='account_name', repeat=True))
4470 def email_update(self, operator, uname):
4471 acc = self._get_account(uname)
4472 acc.update_email_addresses()
4473 return "OK, updated e-mail address for '%s'" % uname
4474
4475 # (email virus)
4476
4477 def _get_email_target_and_address(self, address):
4478 """Returns a tuple consisting of the email target associated
4479 with address and the address object. If there is no at-sign
4480 in address, assume it is an account name and return primary
4481 address. Raises CerebrumError if address is unknown.
4482 """
4483 et = Email.EmailTarget(self.db)
4484 ea = Email.EmailAddress(self.db)
4485 if address.count('@') == 0:
4486 acc = self.Account_class(self.db)
4487 try:
4488 acc.find_by_name(address)
4489 # FIXME: We can't use Account.get_primary_mailaddress
4490 # since it rewrites special domains.
4491 et = Email.EmailTarget(self.db)
4492 et.find_by_target_entity(acc.entity_id)
4493 epa = Email.EmailPrimaryAddressTarget(self.db)
4494 epa.find(et.entity_id)
4495 ea.find(epa.email_primaddr_id)
4496 except Errors.NotFoundError:
4497 try:
4498 dlgroup = Utils.Factory.get("DistributionGroup")(self.db)
4499 dlgroup.find_by_name(address)
4500 et = Email.EmailTarget(self.db)
4501 et.find_by_target_entity(dlgroup.entity_id)
4502 epa = Email.EmailPrimaryAddressTarget(self.db)
4503 epa.find(et.entity_id)
4504 ea.find(epa.email_primaddr_id)
4505 except Errors.NotFoundError:
4506 raise CerebrumError, ("No such address: '%s'" % address)
4507 elif address.count('@') == 1:
4508 try:
4509 ea.find_by_address(address)
4510 et.find(ea.email_addr_target_id)
4511 except Errors.NotFoundError:
4512 raise CerebrumError, "No such address: '%s'" % address
4513 else:
4514 raise CerebrumError, "Malformed e-mail address (%s)" % address
4515 return et, ea
4516
4517 def _get_email_target_and_account(self, address):
4518 """Returns a tuple consisting of the email target associated
4519 with address and the account if the target type is user. If
4520 there is no at-sign in address, assume it is an account name.
4521 Raises CerebrumError if address is unknown."""
4522 et, ea = self._get_email_target_and_address(address)
4523 acc = None
4524 if et.email_target_type in (self.const.email_target_account,
4525 self.const.email_target_deleted):
4526 acc = self._get_account(et.email_target_entity_id, idtype='id')
4527 return et, acc
4528
4529 def _get_email_target_and_dlgroup(self, address):
4530 """Returns a tuple consisting of the email target associated
4531 with address and the account if the target type is user. If
4532 there is no at-sign in address, assume it is an account name.
4533 Raises CerebrumError if address is unknown."""
4534 et, ea = self._get_email_target_and_address(address)
4535 grp = None
4536 # what will happen if the target was a dl_group but is now
4537 # deleted? it's possible that we should have created a new
4538 # target_type = dlgroup_deleted, but it seemed redundant earlier
4539 # now, i'm not so sure (Jazz, 2013-12(
4540 if et.email_target_type in (self.const.email_target_dl_group,
4541 self.const.email_target_deleted):
4542 grp = self._get_group(et.email_target_entity_id, idtype='id',
4543 grtype="DistributionGroup")
4544 return et, grp
4545
4546 def _get_address(self, etarget):
4547 """The argument can be
4548 - EmailPrimaryAddressTarget
4549 - EmailAddress
4550 - EmailTarget (look up primary address and return that, throw
4551 exception if there is no primary address)
4552 - integer (use as entity_id and look up that target's
4553 primary address)
4554 The return value is a text string containing the e-mail
4555 address. Special domain names are not rewritten."""
4556 ea = Email.EmailAddress(self.db)
4557 if isinstance(etarget, (int, long, float)):
4558 epat = Email.EmailPrimaryAddressTarget(self.db)
4559 # may throw exception, let caller handle it
4560 epat.find(etarget)
4561 ea.find(epat.email_primaddr_id)
4562 elif isinstance(etarget, Email.EmailTarget):
4563 epat = Email.EmailPrimaryAddressTarget(self.db)
4564 epat.find(etarget.entity_id)
4565 ea.find(epat.email_primaddr_id)
4566 elif isinstance(etarget, Email.EmailPrimaryAddressTarget):
4567 ea.find(etarget.email_primaddr_id)
4568 elif isinstance(etarget, Email.EmailAddress):
4569 ea = etarget
4570 else:
4571 raise ValueError, "Unknown argument (%s)" % repr(etarget)
4572 ed = Email.EmailDomain(self.db)
4573 ed.find(ea.email_addr_domain_id)
4574 return ("%s@%s" % (ea.email_addr_local_part,
4575 ed.email_domain_name))
4576
4577 #
4578 # entity commands
4579 #
4580
4581 # entity info
4582 all_commands['entity_info'] = None
4583 def entity_info(self, operator, entity_id):
4584 """Returns basic information on the given entity id"""
4585 entity = self._get_entity(ident=entity_id)
4586 return self._entity_info(entity)
4587
4588 def _entity_info(self, entity):
4589 result = {}
4590 co = self.const
4591 result['type'] = str(co.EntityType(entity.entity_type))
4592 result['entity_id'] = entity.entity_id
4593 if entity.entity_type in \
4594 (co.entity_group, co.entity_account):
4595 result['creator_id'] = entity.creator_id
4596 result['create_date'] = entity.created_at
4597 result['expire_date'] = entity.expire_date
4598 # FIXME: Should be a list instead of a string, but text
4599 # clients doesn't know how to view such a list
4600 result['spread'] = ", ".join([str(co.Spread(r['spread']))
4601 for r in entity.get_spread()])
4602 if entity.entity_type == co.entity_group:
4603 result['name'] = entity.group_name
4604 result['description'] = entity.description
4605 result['visibility'] = entity.visibility
4606 try:
4607 result['gid'] = entity.posix_gid
4608 except AttributeError:
4609 pass
4610 elif entity.entity_type == co.entity_account:
4611 result['name'] = entity.account_name
4612 result['owner_id'] = entity.owner_id
4613 #result['home'] = entity.home
4614 # TODO: de-reference disk_id
4615 #result['disk_id'] = entity.disk_id
4616 # TODO: de-reference np_type
4617 # result['np_type'] = entity.np_type
4618 elif entity.entity_type == co.entity_person:
4619 result['name'] = entity.get_name(co.system_cached,
4620 getattr(co, cereconf.DEFAULT_GECOS_NAME))
4621 result['export_id'] = entity.export_id
4622 result['birthdate'] = entity.birth_date
4623 result['description'] = entity.description
4624 result['gender'] = str(co.Gender(entity.gender))
4625 # make boolean
4626 result['deceased'] = entity.deceased_date
4627 names = []
4628 for name in entity.get_all_names():
4629 source_system = str(co.AuthoritativeSystem(name.source_system))
4630 name_variant = str(co.PersonName(name.name_variant))
4631 names.append((source_system, name_variant, name.name))
4632 result['names'] = names
4633 affiliations = []
4634 for row in entity.get_affiliations():
4635 affiliation = {}
4636 affiliation['ou'] = row['ou_id']
4637 affiliation['affiliation'] = str(co.PersonAffiliation(row.affiliation))
4638 affiliation['status'] = str(co.PersonAffStatus(row.status))
4639 affiliation['source_system'] = str(co.AuthoritativeSystem(row.source_system))
4640 affiliations.append(affiliation)
4641 result['affiliations'] = affiliations
4642 elif entity.entity_type == co.entity_ou:
4643 for attr in '''name acronym short_name display_name
4644 sort_name'''.split():
4645 result[attr] = getattr(entity, attr)
4646
4647 return result
4648
4649 # entity accounts
4650 all_commands['entity_accounts'] = Command(
4651 ("entity", "accounts"), EntityType(default="person"), Id(),
4652 fs=FormatSuggestion("%7i %-10s %s", ("account_id", "name", format_day("expire")),
4653 hdr="%7s %-10s %s" % ("Id", "Name", "Expire")))
4654 def entity_accounts(self, operator, entity_type, id):
4655 entity = self._get_entity(entity_type, id)
4656 account = self.Account_class(self.db)
4657 ret = []
4658 for r in account.list_accounts_by_owner_id(entity.entity_id,
4659 entity.entity_type,
4660 filter_expired=False):
4661 account = self._get_account(r['account_id'], idtype='id')
4662
4663 ret.append({'account_id': r['account_id'],
4664 'name': account.account_name,
4665 'expire': account.expire_date})
4666 return ret
4667
4668 # entity history
4669 all_commands['entity_history'] = Command(
4670 ("entity", "history"),
4671 Id(help_ref="id:target:account"),
4672 YesNo(help_ref='yes_no_all_op', optional=True, default="no"),
4673 Integer(optional=True, help_ref="limit_number_of_results"),
4674 fs=FormatSuggestion("%s [%s]: %s",
4675 ("timestamp", "change_by", "message")),
4676 perm_filter='can_show_history')
4677 def entity_history(self, operator, entity, any="no", limit=100):
4678 ent = self.util.get_target(entity, restrict_to=[])
4679 self.ba.can_show_history(operator.get_entity_id(), ent)
4680 ret = []
4681 if self._get_boolean(any):
4682 kw = {'any_entity': ent.entity_id}
4683 else:
4684 kw = {'subject_entity': ent.entity_id}
4685 rows = list(self.db.get_log_events(0, **kw))
4686 try:
4687 limit = int(limit)
4688 except ValueError:
4689 raise CerebrumError, "Limit must be a number"
4690
4691 for r in rows[-limit:]:
4692 ret.append(self._format_changelog_entry(r))
4693
4694 return ret
4695
4696
4697 #
4698 # group commands
4699 #
4700
4701 # FIXME - group_multi_add should later be renamed to group_add, when there's
4702 # enough time. group_padd and group_gadd should be removed as soon as
4703 # the other institutions doesn't depend on them any more.
4704
4705 # group multi_add
4706 # jokim 2008-12-02 TBD: won't let it be used by jbofh, only wofh for now
4707 hidden_commands['group_multi_add'] = Command(
4708 ('group', 'multi_add'),
4709 MemberType(help_ref='member_type', default='account'),
4710 MemberName(help_ref='member_name_src', repeat=True),
4711 GroupName(help_ref='group_name_dest', repeat=True),
4712 perm_filter='can_alter_group')
4713 def group_multi_add(self, operator, member_type, src_name, dest_group):
4714 '''Adds a person, account or group to a given group.'''
4715
4716 if member_type not in ('group', 'account', 'person', ):
4717 raise CerebrumError("Unknown member_type: %s" % (member_type))
4718
4719 return self._group_add(operator, src_name, dest_group,
4720 member_type=member_type)
4721
4722
4723 # group add
4724 all_commands['group_add'] = Command(
4725 ("group", "add"), AccountName(help_ref="account_name_src", repeat=True),
4726 GroupName(help_ref="group_name_dest", repeat=True),
4727 perm_filter='can_alter_group')
4728 def group_add(self, operator, src_name, dest_group):
4729 return self._group_add(operator, src_name, dest_group,
4730 member_type="account")
4731
4732 # group padd - add person to group
4733 all_commands['group_padd'] = Command(
4734 ("group", "padd"), PersonId(help_ref="id:target:person", repeat=True),
4735 GroupName(help_ref="group_name_dest", repeat=True),
4736 perm_filter='can_alter_group')
4737 def group_padd(self, operator, src_name, dest_group):
4738 return self._group_add(operator, src_name, dest_group,
4739 member_type="person")
4740 # group gadd
4741 all_commands['group_gadd'] = Command(
4742 ("group", "gadd"), GroupName(help_ref="group_name_src", repeat=True),
4743 GroupName(help_ref="group_name_dest", repeat=True),
4744 perm_filter='can_alter_group')
4745 def group_gadd(self, operator, src_name, dest_group):
4746 return self._group_add(operator, src_name, dest_group,
4747 member_type="group")
4748
4749 def _group_add(self, operator, src_name, dest_group, member_type=None):
4750 if member_type == "group":
4751 src_entity = self._get_group(src_name)
4752 elif member_type == "account":
4753 src_entity = self._get_account(src_name)
4754 elif member_type == "person":
4755 try:
4756 src_entity = self.util.get_target(src_name,
4757 restrict_to=['Person'])
4758 except Errors.TooManyRowsError:
4759 raise CerebrumError("Unexpectedly found more than one person")
4760 return self._group_add_entity(operator, src_entity, dest_group)
4761
4762 def _group_add_entity(self, operator, src_entity, dest_group):
4763 group_d = self._get_group(dest_group)
4764 if operator:
4765 self.ba.can_alter_group(operator.get_entity_id(), group_d)
4766 src_name = self._get_name_from_object(src_entity)
4767 # Make the error message for the most common operator error
4768 # more friendly. Don't treat this as an error, useful if the
4769 # operator has specified more than one entity.
4770 if group_d.has_member(src_entity.entity_id):
4771 return "%s is already a member of %s" % (src_name, dest_group)
4772 # Make sure that the src_entity does not have group_d as a
4773 # member already, to avoid a recursion well at export
4774 if src_entity.entity_type == self.const.entity_group:
4775 for row in src_entity.search_members(member_id=group_d.entity_id,
4776 member_type=self.const.entity_group,
4777 indirect_members=True,
4778 member_filter_expired=False):
4779 if row['group_id'] == src_entity.entity_id:
4780 return "Recursive memberships are not allowed (%s is member of %s)" % (dest_group, src_name)
4781 # This can still fail, e.g., if the entity is a member with a
4782 # different operation.
4783 try:
4784 group_d.add_member(src_entity.entity_id)
4785 except self.db.DatabaseError, m:
4786 raise CerebrumError, "Database error: %s" % m
4787 # Warn the user about NFS filegroup limitations.
4788 nis_warning = ''
4789 for spread_name in cereconf.NIS_SPREADS:
4790 fg_spread = getattr(self.const, spread_name)
4791 for row in group_d.get_spread():
4792 if row['spread'] == fg_spread:
4793 count = self._group_count_memberships(src_entity.entity_id,
4794 fg_spread)
4795 if count > 16:
4796 nis_warning = (
4797 'OK, added {source_name} to {group}\n'
4798 'WARNING: {source_name} is now a member of '
4799 '{amount_groups} NIS groups with spread {spread}.'
4800 '\nActual membership lookups in NIS may not work '
4801 'as expected if a user is member of more than 16 '
4802 'NIS groups.'.format(source_name=src_name,
4803 amount_groups=count,
4804 spread=fg_spread,
4805 group=dest_group))
4806 if nis_warning:
4807 return nis_warning
4808 return 'OK, added {source_name} to {group}'.format(
4809 source_name=src_name,
4810 group=dest_group)
4811
4812 def _group_count_memberships(self, entity_id, spread):
4813 """Count how many groups of a given spread have entity_id as a member,
4814 either directly or indirectly."""
4815
4816 gr = Utils.Factory.get("Group")(self.db)
4817 groups = list(gr.search(member_id=entity_id,
4818 indirect_members=True,
4819 spread=spread))
4820 return len(groups)
4821 # end _group_count_memberships
4822
4823
4824 # group add_entity
4825 all_commands['group_add_entity'] = None
4826 def group_add_entity(self, operator, src_entity_id, dest_group_id):
4827 """Adds a entity to a group. Both the source entity and the group
4828 should be entity IDs"""
4829 # tell _group_find later on that dest_group is a entity id
4830 dest_group = 'id:%s' % dest_group_id
4831 src_entity = self._get_entity(ident=src_entity_id)
4832 if not src_entity.entity_type in \
4833 (self.const.entity_account, self.const.entity_group):
4834 raise CerebrumError, \
4835 "Entity %s is not a legal type " \
4836 "to become group member" % src_entity_id
4837 return self._group_add_entity(operator, src_entity, dest_group)
4838
4839 # group exchange_create
4840 all_commands['group_exchange_create'] = Command(
4841 ("group", "exchange_create"),
4842 GroupName(help_ref="group_name_new"),
4843 SimpleString(help_ref="group_disp_name", optional='true'),
4844 SimpleString(help_ref="string_dl_desc"),
4845 YesNo(help_ref='yes_no_from_existing', default='No'),
4846 fs=FormatSuggestion("Group created, internal id: %i", ("group_id",)),
4847 perm_filter='is_postmaster')
4848 def group_exchange_create(self, operator, groupname, displayname, description, from_existing=None):
4849 if not self.ba.is_postmaster(operator.get_entity_id()):
4850 raise PermissionDenied('No access to group')
4851 existing_group = False
4852 dl_group = Utils.Factory.get("DistributionGroup")(self.db)
4853 std_values = dl_group.ret_standard_attr_values(room=False)
4854 # although cerebrum supports different visibility levels
4855 # all groups are created visibile for all, and that vis
4856 # type is hardcoded. if the situation should change group
4857 # vis may be made into a parameter
4858 group_vis = self.const.group_visibility_all
4859 # display name language is standard for dist groups
4860 disp_name_language = dl_group.ret_standard_language()
4861 disp_name_variant = self.const.dl_group_displ_name
4862 managedby = cereconf.DISTGROUP_DEFAULT_ADMIN
4863 grp = Utils.Factory.get("Group")(self.db)
4864 try:
4865 grp.find_by_name(groupname)
4866 existing_group = True
4867 except Errors.NotFoundError:
4868 # nothing to do, inconsistencies are dealt with
4869 # further down
4870 pass
4871 if not displayname:
4872 displayname = groupname
4873 if existing_group and not self._is_yes(from_existing):
4874 return ('You choose not to create Exchange group from the '
4875 'existing group %s' % groupname)
4876 try:
4877 if not existing_group:
4878 # one could imagine making a helper function in the future
4879 # _make_dl_group_new, as the functionality is required
4880 # both here and for the roomlist creation (Jazz, 2013-12)
4881 dl_group.new(operator.get_entity_id(),
4882 group_vis,
4883 groupname, description=description,
4884 roomlist=std_values['roomlist'],
4885 hidden=std_values['hidden'])
4886 else:
4887 dl_group.populate(roomlist=std_values['roomlist'],
4888 hidden=std_values['hidden'],
4889 parent=grp)
4890 dl_group.write_db()
4891 except self.db.DatabaseError, m:
4892 raise CerebrumError, "Database error: %s" % m
4893 self._set_display_name(groupname, displayname,
4894 disp_name_variant, disp_name_language)
4895 dl_group.create_distgroup_mailtarget()
4896 dl_group.add_spread(self.const.Spread(cereconf.EXCHANGE_GROUP_SPREAD))
4897 dl_group.write_db()
4898 return "Created Exchange group %s" % groupname
4899
4900 # group exchange_info
4901 all_commands['group_exchange_info'] = Command(
4902 ("group", "exchange_info"), GroupName(help_ref="id:gid:name"),
4903 fs=FormatSuggestion([("Name: %s\n" +
4904 "Spreads: %s\n" +
4905 "Description: %s\n" +
4906 "Expire: %s\n" +
4907 "Entity id: %i""",
4908 ("name", "spread", "description",
4909 format_day("expire_date"),
4910 "entity_id")),
4911 ("Moderator: %s %s (%s)",
4912 ('owner_type', 'owner', 'opset')),
4913 ("Gid: %i",
4914 ('gid',)),
4915 ("Members: %s", ("members",)),
4916
4917 ("DisplayName: %s",
4918 ('displayname',)),
4919 ("Roomlist: %s",
4920 ('roomlist',)),
4921 ("Hidden: %s",
4922 ('hidden',)),
4923 ("PrimaryAddr: %s",
4924 ('primary',)),
4925 ("Aliases: %s",
4926 ('aliases_1',)),
4927 (" %s",
4928 ('aliases',))]))
4929 def group_exchange_info(self, operator, groupname):
4930 if not self.ba.is_postmaster(operator.get_entity_id()):
4931 raise PermissionDenied('No access to group')
4932
4933 co = self.const
4934 grp = self._get_group(groupname, grtype="DistributionGroup")
4935 gr_info = self._entity_info(grp)
4936
4937 # Don't stop! Never give up!
4938 # We just delete stuff, thats faster to implement than fixing stuff.
4939 del gr_info['create_date']
4940 del gr_info['visibility']
4941 del gr_info['creator_id']
4942 del gr_info['type']
4943 ret = [ gr_info ]
4944
4945 # find owners
4946 aot = BofhdAuthOpTarget(self.db)
4947 targets = []
4948 for row in aot.list(target_type='group', entity_id=grp.entity_id):
4949 targets.append(int(row['op_target_id']))
4950 ar = BofhdAuthRole(self.db)
4951 aos = BofhdAuthOpSet(self.db)
4952 for row in ar.list_owners(targets):
4953 aos.clear()
4954 aos.find(row['op_set_id'])
4955 id = int(row['entity_id'])
4956 en = self._get_entity(ident=id)
4957 if en.entity_type == co.entity_account:
4958 owner = en.account_name
4959 elif en.entity_type == co.entity_group:
4960 owner = en.group_name
4961 else:
4962 owner = '#%d' % id
4963 ret.append({'owner_type': str(co.EntityType(en.entity_type)),
4964 'owner': owner,
4965 'opset': aos.name})
4966
4967
4968 # Member stats are a bit complex, since any entity may be a
4969 # member. Collect them all and sort them by members.
4970 members = dict()
4971 for row in grp.search_members(group_id=grp.entity_id):
4972 members[row["member_type"]] = members.get(row["member_type"], 0) + 1
4973
4974 # Produce a list of members sorted by member type
4975 ET = self.const.EntityType
4976 entries = ["%d %s(s)" % (members[x], str(ET(x)))
4977 for x in sorted(members,
4978 lambda it1, it2:
4979 cmp(str(ET(it1)),
4980 str(ET(it2))))]
4981
4982 ret.append({"members": ", ".join(entries)})
4983 # Find distgroup info
4984 roomlist = True if grp.roomlist == 'T' else False
4985 dgr_info = grp.get_distgroup_attributes_and_targetdata(
4986 roomlist=roomlist)
4987 del dgr_info['group_id']
4988 del dgr_info['name']
4989 del dgr_info['description']
4990
4991 # Yes, I'm gonna do it!
4992 tmp = {}
4993 for attr in ['displayname', 'roomlist']:
4994 if attr in dgr_info:
4995 tmp[attr] = dgr_info[attr]
4996 ret.append(tmp)
4997
4998 tmp = {}
4999 for attr in ['hidden', 'primary']:
5000 if attr in dgr_info:
5001 tmp[attr] = dgr_info[attr]
5002 ret.append(tmp)
5003
5004 if dgr_info.has_key('aliases'):
5005 if len(dgr_info['aliases']) > 0:
5006 ret.append({'aliases_1': dgr_info['aliases'].pop(0)})
5007
5008 for alias in dgr_info['aliases']:
5009 ret.append({'aliases': alias})
5010
5011 return ret
5012
5013 # group exchange_remove
5014 all_commands['group_exchange_remove'] = Command(
5015 ("group", "exchange_remove"),
5016 GroupName(help_ref="group_name", repeat='true'),
5017 YesNo(help_ref='yes_no_expire_group', default='No'),
5018 perm_filter='is_postmaster')
5019 def group_exchange_remove(self, operator, groupname, expire_group=None):
5020 # check for appropriate priviledge
5021 if not self.ba.is_postmaster(operator.get_entity_id()):
5022 raise PermissionDenied('No access to group')
5023 dl_group = self._get_group(groupname, idtype='name',
5024 grtype="DistributionGroup")
5025 try:
5026 dl_group.delete_spread(self.const.Spread(cereconf.EXCHANGE_GROUP_SPREAD))
5027 dl_group.deactivate_dl_mailtarget()
5028 dl_group.demote_distribution()
5029 except Errors.NotFoundError:
5030 return "No Exchange group %s found" % groupname
5031 if self._is_yes(expire_group):
5032 # set expire in 90 dates for the remaining Cerebrum-group
5033 new_expire_date = DateTime.now() + DateTime.DateTimeDelta(90, 0, 0)
5034 dl_group.expire_date = new_expire_date
5035 dl_group.write_db()
5036 return "Exchange group data removed for %s" % groupname
5037
5038 # group exchange_visibility
5039 all_commands['group_exchange_visibility'] = Command(
5040 ("group", "exchange_visibility"),
5041 GroupName(help_ref="group_name"),
5042 YesNo(optional=False, help_ref='yes_no_visible'),
5043 perm_filter='is_postmaster')
5044 def group_exchange_visibility(self, operator, groupname, visible):
5045 if not self.ba.is_postmaster(operator.get_entity_id()):
5046 raise PermissionDenied('No access to group')
5047 dl_group = self._get_group(groupname, idtype='name',
5048 grtype="DistributionGroup")
5049 visible = self._get_boolean(visible)
5050 dl_group.set_hidden(hidden='F' if visible else 'T')
5051 dl_group.write_db()
5052 return "OK, group {} is now {}".format(
5053 groupname, 'visible' if visible else 'hidden')
5054
5055 # create roomlists, which are a special kind of distribution group
5056 # no re-use of existing groups allowed
5057 all_commands['group_roomlist_create'] = Command(
5058 ("group", "roomlist_create"),
5059 GroupName(help_ref="group_name_new"),
5060 SimpleString(help_ref="group_disp_name", optional='true'),
5061 SimpleString(help_ref="string_description"),
5062 fs=FormatSuggestion("Group created, internal id: %i", ("group_id",)),
5063 perm_filter='is_postmaster')
5064
5065 def group_roomlist_create(self, operator, groupname, displayname,
5066 description):
5067 """Create a new roomlist for Exchange."""
5068 # check for appropriate priviledge
5069 if not self.ba.is_postmaster(operator.get_entity_id()):
5070 raise PermissionDenied('No access to group')
5071 grp = Utils.Factory.get("Group")(self.db)
5072 try:
5073 grp.find_by_name(groupname)
5074 return "Cannot make an existing group into a roomlist"
5075 except Errors.NotFoundError:
5076 pass
5077 room_list = Utils.Factory.get("DistributionGroup")(self.db)
5078 std_values = room_list.ret_standard_attr_values(room=True)
5079 # although cerebrum supports different visibility levels
5080 # all groups are created visibile for all, and that vis
5081 # type is hardcoded. if the situation should change group
5082 # vis may be made into a parameter
5083 group_vis = self.const.group_visibility_all
5084 # the following attributes is not used and don't need to
5085 # be registered correctly
5086 # managedby is never exported to Exchange, hardcoded to
5087 # dl-dladmin@groups.uio.bo
5088 managedby = cereconf.DISTGROUP_DEFAULT_ADMIN
5089 # display name language is standard for dist groups
5090 disp_name_language = room_list.ret_standard_language()
5091 disp_name_variant = self.const.dl_group_displ_name
5092 # we could use _valid_address_exchange here in stead,
5093 # I'll leave as an exercise for a willing developer
5094 # :-) (Jazz, 2013-12)
5095 ea = Email.EmailAddress(self.db)
5096 try:
5097 ea.find_by_address(managedby)
5098 except Errors.NotFoundError:
5099 # should never happen unless default admin
5100 # dist group is deleted from Cerebrum
5101 return ('Default admin address does not exist, please contact'
5102 ' bas-admin@cc.uit.no for help!')
5103 if not displayname:
5104 displayname = groupname
5105 # using DistributionGroup.new(...)
5106 room_list.new(operator.get_entity_id(),
5107 group_vis,
5108 groupname, description=description,
5109 roomlist=std_values['roomlist'],
5110 hidden=std_values['hidden'])
5111 room_list.write_db()
5112 room_list.add_spread(self.const.Spread(cereconf.EXCHANGE_GROUP_SPREAD))
5113 self._set_display_name(groupname, displayname, disp_name_variant,
5114 disp_name_language)
5115 room_list.write_db()
5116
5117 # Try to set the default group moderator
5118 try:
5119 grp.clear()
5120 grp.find_by_name(cereconf.EXCHANGE_ROOMLIST_OWNER)
5121 except (Errors.NotFoundError, AttributeError):
5122 # If the group moderator group does not exist, or is not defined,
5123 # we won't set a group owner.
5124 pass
5125 else:
5126 op_set = BofhdAuthOpSet(self.db)
5127 op_set.find_by_name(cereconf.BOFHD_AUTH_GROUPMODERATOR)
5128 op_target = BofhdAuthOpTarget(self.db)
5129 op_target.populate(room_list.entity_id, 'group')
5130 op_target.write_db()
5131 role = BofhdAuthRole(self.db)
5132 role.grant_auth(grp.entity_id, op_set.op_set_id,
5133 op_target.op_target_id)
5134
5135 return "Made roomlist %s" % groupname
5136
5137 ## group create
5138 # (all_commands is updated from BofhdCommonMethods)
5139 def group_create(self, operator, groupname, description):
5140 """Override group_create to double check that there doesn't exist an
5141 account with the same name.
5142 """
5143 ac = self.Account_class(self.db)
5144 try:
5145 ac.find_by_name(groupname)
5146 except Errors.NotFoundError:
5147 pass
5148 else:
5149 raise CerebrumError('An account exists with name: %s' % groupname)
5150 return super(BofhdExtension, self).group_create(operator, groupname,
5151 description)
5152
5153 # group request, like group create, but only send request to
5154 # the ones with the access to the 'group create' command
5155 # Currently send email to brukerreg@usit.uio.no
5156 all_commands['group_request'] = Command(
5157 ("group", "request"), GroupName(help_ref="group_name_new"),
5158 SimpleString(help_ref="string_description"), SimpleString(help_ref="string_spread"),
5159 GroupName(help_ref="group_name_moderator"))
5160
5161 def group_request(self, operator, groupname, description, spread, moderator):
5162 opr = operator.get_entity_id()
5163 acc = self.Account_class(self.db)
5164 acc.find(opr)
5165
5166 # checking if group already exists
5167 try:
5168 self._get_group(groupname)
5169 except CerebrumError:
5170 pass
5171 else:
5172 raise CerebrumError("Group %s already exists" % (groupname))
5173
5174 # checking if moderator groups exist
5175 for mod in moderator.split(' '):
5176 try:
5177 self._get_group(mod)
5178 except CerebrumError:
5179 raise CerebrumError("Moderator group %s not found" % (mod))
5180
5181 fromaddr = acc.get_primary_mailaddress()
5182 toaddr = cereconf.GROUP_REQUESTS_SENDTO
5183 if spread is None: spread = ""
5184 spreadstring = "(" + spread + ")"
5185 spreads = []
5186 spreads = re.split(" ", spread)
5187 subject = "Cerebrum group create request %s" % groupname
5188 body = []
5189 body.append("Please create a new group:")
5190 body.append("")
5191 body.append("Group-name: %s." % groupname)
5192 body.append("Description: %s" % description)
5193 body.append("Requested by: %s" % fromaddr)
5194 body.append("Moderator: %s" % moderator)
5195 body.append("")
5196 body.append("group create %s \"%s\"" % (groupname, description))
5197 for spr in spreads:
5198 if spr and (self._get_constant(self.const.Spread, spr) in
5199 [self.const.spread_uit_nis_fg, self.const.spread_ifi_nis_fg,
5200 self.const.spread_hpc_nis_fg]):
5201 pg = Utils.Factory.get('PosixGroup')(self.db)
5202 err_str = pg.illegal_name(groupname)
5203 if err_str:
5204 if not isinstance(err_str, basestring): # paranoia
5205 err_str = 'Illegal groupname'
5206 raise CerebrumError('Group-name error: {err_str}'.format(
5207 err_str=err_str))
5208 body.append("group promote_posix %s" % groupname)
5209 if spread:
5210 body.append("spread add group %s %s" % (groupname, spreadstring))
5211 body.append("access grant Group-owner (%s) group %s" % (moderator, groupname))
5212 body.append("group info %s" % groupname)
5213 body.append("")
5214 body.append("")
5215 Utils.sendmail(toaddr, fromaddr, subject, "\n".join(body))
5216 return "Request sent to %s" % toaddr
5217
5218 # group def
5219 all_commands['group_def'] = Command(
5220 ('group', 'def'), AccountName(), GroupName(help_ref="group_name_dest"))
5221
5222 def group_def(self, operator, accountname, groupname):
5223 account = self._get_account(accountname, actype="PosixUser")
5224 grp = self._get_group(groupname, grtype="PosixGroup")
5225 op = operator.get_entity_id()
5226 self.ba.can_set_default_group(op, account, grp)
5227 account.gid_id = grp.entity_id
5228 account.write_db()
5229 return "OK, set default-group for '%s' to '%s'" % (
5230 accountname, groupname)
5231
5232 # group delete
5233 all_commands['group_delete'] = Command(
5234 ("group", "delete"), GroupName(), perm_filter='can_delete_group')
5235
5236 def group_delete(self, operator, groupname):
5237 grp = self._get_group(groupname)
5238 self.ba.can_delete_group(operator.get_entity_id(), grp)
5239 if grp.group_name == cereconf.BOFHD_SUPERUSER_GROUP:
5240 raise CerebrumError("Can't delete superuser group")
5241 # exchange-relatert-jazz
5242 # it should not be possible to remove distribution groups via
5243 # bofh, as that would "orphan" e-mail target. if need be such groups
5244 # should be nuked using a cerebrum-side script.
5245 if grp.has_extension('DistributionGroup'):
5246 raise CerebrumError(
5247 "Cannot delete distribution groups, use 'group"
5248 " exchange_remove' to deactivate %s" % groupname)
5249 elif grp.has_extension('PosixGroup'):
5250 raise CerebrumError(
5251 "Cannot delete posix groups, use 'group demote_posix %s'"
5252 " before deleting." % groupname)
5253 elif grp.get_extensions():
5254 raise CerebrumError(
5255 "Cannot delete group %s, is type %r" % (groupname,
5256 grp.get_extensions()))
5257
5258 self._remove_auth_target("group", grp.entity_id)
5259 self._remove_auth_role(grp.entity_id)
5260 try:
5261 grp.delete()
5262 except self.db.DatabaseError, msg:
5263 if re.search("group_member_exists", str(msg)):
5264 raise CerebrumError(
5265 ("Group is member of groups. "
5266 "Use 'group memberships group %s'") % grp.group_name)
5267 elif re.search("account_info_owner", str(msg)):
5268 raise CerebrumError(
5269 ("Group is owner of an account. "
5270 "Use 'entity accounts group %s'") % grp.group_name)
5271 raise
5272 return "OK, deleted group '%s'" % groupname
5273
5274 # group multi_remove
5275 # jokim 2008-12-02 TBD: removed from jbofh, but not wofh
5276 hidden_commands['group_multi_remove'] = Command(
5277 ("group", "multi_remove"),
5278 MemberType(help_ref='member_type', default='account'),
5279 MemberName(help_ref="member_name_src", repeat=True),
5280 GroupName(help_ref="group_name_dest", repeat=True),
5281 perm_filter='can_alter_group')
5282 def group_multi_remove(self, operator, member_type, src_name, dest_group):
5283 '''Removes a person, account or group from a given group.'''
5284
5285 if member_type not in ('group', 'account', 'person', ):
5286 return 'Unknown member_type "%s"' % (member_type)
5287 self.ba.can_alter_group(operator.get_entity_id(),
5288 self._get_group(dest_group))
5289 return self._group_remove(operator, src_name, dest_group,
5290 member_type=member_type)
5291
5292 # FIXME - group_remove and group_gremove is now handled by
5293 # group_multi_remove(membertype='group'...), and should be removed as soon as the
5294 # other institutions has updated their dependency. group_multi_remove should then
5295 # be renamed to group_remove.
5296
5297 # group remove
5298 all_commands['group_remove'] = Command(
5299 ("group", "remove"), AccountName(help_ref="account_name_member", repeat=True),
5300 GroupName(help_ref="group_name_dest", repeat=True))
5301 def group_remove(self, operator, src_name, dest_group):
5302 try:
5303 # First, check if this is a user we can set the password
5304 # for; if so, we should be allowed to remove this user
5305 # from groups, e.g. if we have LITA rights for the account
5306 account = self._get_account(src_name)
5307 self.ba.can_set_password(operator.get_entity_id(), account)
5308 except PermissionDenied, pd:
5309 # If that fails; check if we have rights pertaining to the
5310 # group in question
5311 group = self._get_group(dest_group)
5312 self.ba.can_alter_group(operator.get_entity_id(), group)
5313 return self._group_remove(operator, src_name, dest_group,
5314 member_type="account")
5315
5316 # group gremove
5317 all_commands['group_gremove'] = Command(
5318 ("group", "gremove"), GroupName(help_ref="group_name_src", repeat=True),
5319 GroupName(help_ref="group_name_dest", repeat=True),
5320 perm_filter='can_alter_group')
5321 def group_gremove(self, operator, src_name, dest_group):
5322 self.ba.can_alter_group(operator.get_entity_id(),
5323 self._get_group(dest_group))
5324 return self._group_remove(operator, src_name, dest_group,
5325 member_type="group")
5326
5327 # group premove
5328 all_commands['group_premove'] = Command(
5329 ("group", "premove"), MemberName(help_ref='member_name_src', repeat=True),
5330 GroupName(help_ref="group_name_dest", repeat=True),
5331 perm_filter='can_alter_group')
5332 def group_premove(self, operator, src_name, dest_group):
5333 self.ba.can_alter_group(operator.get_entity_id(),
5334 self._get_group(dest_group))
5335 return self._group_remove(operator, src_name, dest_group,
5336 member_type="person")
5337
5338 def _group_remove(self, operator, src_name, dest_group, member_type=None):
5339 # jokim 2008-12-02 TBD: Is this bad? Added support for removing
5340 # members by their entity_id, as 'brukerinfo' (wofh) only knows
5341 # the entity_id.
5342 if isinstance(src_name, str) and not src_name.isdigit():
5343 idtype = 'name';
5344 else:
5345 idtype = 'id';
5346
5347 if member_type == "group":
5348 src_entity = self._get_group(src_name, idtype=idtype)
5349 elif member_type == "account":
5350 src_entity = self._get_account(src_name, idtype=idtype)
5351 elif member_type == "person":
5352 if(idtype == 'name'):
5353 idtype = 'account'
5354
5355 try:
5356 src_entity = self.util.get_target(src_name,
5357 default_lookup=idtype, restrict_to=['Person'])
5358 except Errors.TooManyRowsError:
5359 raise CerebrumError("Unexpectedly found more than one person")
5360 else:
5361 raise CerebrumError("Unknown member_type: %s" % member_type)
5362 group_d = self._get_group(dest_group)
5363 return self._group_remove_entity(operator, src_entity, group_d)
5364
5365 def _group_remove_entity(self, operator, member, group):
5366 member_name = self._get_name_from_object(member)
5367 if not group.has_member(member.entity_id):
5368 return ("%s isn't a member of %s" %
5369 (member_name, group.group_name))
5370 if member.entity_type == self.const.entity_account:
5371 try:
5372 pu = Utils.Factory.get('PosixUser')(self.db)
5373 pu.find(member.entity_id)
5374 if pu.gid_id == group.entity_id:
5375 raise CerebrumError("Can't remove %s from primary group %s" %
5376 (member_name, group.group_name))
5377 except Errors.NotFoundError:
5378 pass
5379 try:
5380 group.remove_member(member.entity_id)
5381 except self.db.DatabaseError, m:
5382 raise CerebrumError, "Database error: %s" % m
5383 return "OK, removed '%s' from '%s'" % (member_name, group.group_name)
5384
5385 # group remove_entity
5386 all_commands['group_remove_entity'] = None
5387 def group_remove_entity(self, operator, member_entity, group_entity):
5388 group = self._get_entity(ident=group_entity)
5389 self.ba.can_alter_group(operator.get_entity_id(), group)
5390 member = self._get_entity(ident=member_entity)
5391 return self._group_remove_entity(operator, member, group)
5392
5393
5394 # group info
5395 all_commands['group_info'] = Command(
5396 ("group", "info"), GroupName(help_ref="id:gid:name"),
5397 fs=FormatSuggestion([("Name: %s\n" +
5398 "Spreads: %s\n" +
5399 "Description: %s\n" +
5400 "Expire: %s\n" +
5401 "Entity id: %i""",
5402 ("name", "spread", "description",
5403 format_day("expire_date"),
5404 "entity_id")),
5405 ("Moderator: %s %s (%s)",
5406 ('owner_type', 'owner', 'opset')),
5407 ("Gid: %i",
5408 ('gid',)),
5409 ("Members: %s", ("members",))]))
5410 def group_info(self, operator, groupname):
5411 # TODO: Group visibility should probably be checked against
5412 # operator for a number of commands
5413 try:
5414 grp = self._get_group(groupname, grtype="PosixGroup")
5415 except CerebrumError:
5416 if groupname.startswith('gid:'):
5417 gid = groupname.split(':',1)[1]
5418 raise CerebrumError("Could not find PosixGroup with gid=%s" % gid)
5419 grp = self._get_group(groupname)
5420 co = self.const
5421 ret = [ self._entity_info(grp) ]
5422 # find owners
5423 aot = BofhdAuthOpTarget(self.db)
5424 targets = []
5425 for row in aot.list(target_type='group', entity_id=grp.entity_id):
5426 targets.append(int(row['op_target_id']))
5427 ar = BofhdAuthRole(self.db)
5428 aos = BofhdAuthOpSet(self.db)
5429 for row in ar.list_owners(targets):
5430 aos.clear()
5431 aos.find(row['op_set_id'])
5432 id = int(row['entity_id'])
5433 en = self._get_entity(ident=id)
5434 if en.entity_type == co.entity_account:
5435 owner = en.account_name
5436 elif en.entity_type == co.entity_group:
5437 owner = en.group_name
5438 else:
5439 owner = '#%d' % id
5440 ret.append({'owner_type': str(co.EntityType(en.entity_type)),
5441 'owner': owner,
5442 'opset': aos.name})
5443
5444
5445 # Member stats are a bit complex, since any entity may be a
5446 # member. Collect them all and sort them by members.
5447 members = dict()
5448 for row in grp.search_members(group_id=grp.entity_id):
5449 members[row["member_type"]] = members.get(row["member_type"], 0) + 1
5450
5451 # Produce a list of members sorted by member type
5452 ET = self.const.EntityType
5453 entries = ["%d %s(s)" % (members[x], str(ET(x)))
5454 for x in sorted(members,
5455 lambda it1, it2:
5456 cmp(str(ET(it1)),
5457 str(ET(it2))))]
5458
5459 ret.append({"members": ", ".join(entries)})
5460 return ret
5461 # end group_info
5462
5463
5464 # group list
5465 all_commands['group_list'] = Command(
5466 ("group", "list"), GroupName(),
5467 fs=FormatSuggestion("%-10s %-15s %-45s %-10s", ("type",
5468 "user_name",
5469 "full_name",
5470 "expired"),
5471 hdr="%-10s %-15s %-45s %-10s" % ("Type",
5472 "Username",
5473 "Fullname",
5474 "Expired")))
5475 def group_list(self, operator, groupname):
5476 """List direct members of group"""
5477 def compare(a, b):
5478 return cmp(a['type'], b['type']) or \
5479 cmp(a['user_name'], b['user_name'])
5480 group = self._get_group(groupname)
5481 ret = []
5482 now = DateTime.now()
5483 members = list(group.search_members(group_id=group.entity_id,
5484 indirect_members=False,
5485 member_filter_expired=False))
5486 if len(members) > cereconf.BOFHD_MAX_MATCHES and not self.ba.is_superuser(operator.get_entity_id()):
5487 raise CerebrumError("More than %d (%d) matches. Contact superuser "
5488 "to get a listing for %s." %
5489 (cereconf.BOFHD_MAX_MATCHES, len(members), groupname))
5490 ac = self.Account_class(self.db)
5491 pe = Utils.Factory.get('Person')(self.db)
5492 for x in self._fetch_member_names(members):
5493 if x['member_type'] == int(self.const.entity_account):
5494 ac.find(x['member_id'])
5495 try:
5496 pe.find(ac.owner_id)
5497 full_name = pe.get_name(self.const.system_cached,
5498 self.const.name_full)
5499 except Errors.NotFoundError:
5500 full_name = ''
5501 user_name = x['member_name']
5502 ac.clear()
5503 pe.clear()
5504 else:
5505 full_name = x['member_name']
5506 user_name = '<non-account>'
5507 tmp = {'id': x['member_id'],
5508 'type': str(self.const.EntityType(x['member_type'])),
5509 'name': x['member_name'], # Compability with brukerinfo
5510 'user_name': user_name,
5511 'full_name': full_name,
5512 'expired': None}
5513 if x["expire_date"] is not None and x["expire_date"] < now:
5514 tmp["expired"] = "expired"
5515 ret.append(tmp)
5516
5517 ret.sort(compare)
5518 return ret
5519
5520 def _fetch_member_names(self, iterable):
5521 """Locate names for elements in iterable.
5522
5523 This is a convenience method. It helps us to locate names associated
5524 with certain member ids. For group and account members we try to fetch
5525 a name (there is at most one). For all other types we assume no such
5526 name exists.
5527
5528 @type iterable: sequence (any iterable sequence) or a generator.
5529 @param iterable:
5530 A 'iterable' over db_rows that we have to map to names. Each db_row
5531 has a number of keys. This method examines 'member_type' and
5532 'member_id'. All others are ignored.
5533
5534 @rtype: generator (over modified elements of L{iterable})
5535 @return:
5536 A generator over db_rows from L{iterable}. Each db_row gets an
5537 additional key, 'member_name' containing the name of the element or
5538 None, if no name can be located. The relative order of elements in
5539 L{iterable} is preserved. The underlying db_row objects are modified.
5540 """
5541
5542 # TODO: hack to omit bug when inserting new key/value pairs in db_row
5543 ret = []
5544
5545 for item in iterable:
5546 member_type = int(item["member_type"])
5547 member_id = int(item["member_id"])
5548 tmp = item.dict()
5549 tmp["member_name"] = self._get_entity_name(member_id, member_type)
5550 ret.append(tmp)
5551 #yield item
5552 return ret
5553 # end _fetch_member_names
5554
5555
5556 # group list_expanded
5557 all_commands['group_list_expanded'] = Command(
5558 ("group", "list_expanded"), GroupName(),
5559 fs=FormatSuggestion("%8i %10s %30s %25s",
5560 ("member_id", "member_type", "member_name", "group_name"),
5561 hdr="%8s %10s %30s %30s" % ("mem_id", "mem_type",
5562 "member_name",
5563 "is a member of group_name")))
5564 def group_list_expanded(self, operator, groupname):
5565 """List members of group after expansion"""
5566 group = self._get_group(groupname)
5567 result = list()
5568 type2str = lambda x: str(self.const.EntityType(int(x)))
5569 all_members = list(group.search_members(group_id=group.entity_id,
5570 indirect_members=True))
5571 if len(all_members) > cereconf.BOFHD_MAX_MATCHES and not self.ba.is_superuser(operator.get_entity_id()):
5572 raise CerebrumError("More than %d (%d) matches. Contact superuser"
5573 "to get a listing for %s." %
5574 (cereconf.BOFHD_MAX_MATCHES, len(all_members), groupname))
5575 for member in all_members:
5576 member_type = member["member_type"]
5577 member_id = member["member_id"]
5578 result.append({"member_id": member_id,
5579 "member_type": type2str(member_type),
5580 "member_name": self._get_entity_name(int(member_id),
5581 member_type),
5582 "group_name": self._get_entity_name(int(member["group_id"]),
5583 self.const.entity_group),
5584 })
5585 return result
5586 # end group_list_expanded
5587
5588 # group personal <uname>+
5589 all_commands['group_personal'] = Command(
5590 ("group", "personal"), AccountName(repeat=True),
5591 fs=FormatSuggestion(
5592 "Personal group created and made primary, POSIX gid: %i\n"+
5593 "The user may have to wait a minute, then restart bofh to access\n"+
5594 "the 'group add' command", ("group_id",)),
5595 perm_filter='can_create_personal_group')
5596 def group_personal(self, operator, uname):
5597 """This is a separate command for convenience and consistency.
5598 A personal group is always a PosixGroup, and has the same
5599 spreads as the user."""
5600 acc = self._get_account(uname, actype="PosixUser")
5601 op = operator.get_entity_id()
5602 self.ba.can_create_personal_group(op, acc)
5603 # 1. Create group
5604 group = self.Group_class(self.db)
5605 try:
5606 group.find_by_name(uname)
5607 raise CerebrumError, "Group %s already exists" % uname
5608 except Errors.NotFoundError:
5609 group.populate(creator_id=op,
5610 visibility=self.const.group_visibility_all,
5611 name=uname,
5612 description=('Personal file group for %s' % uname))
5613 group.write_db()
5614 # 2. Promote to PosixGroup
5615 pg = Utils.Factory.get('PosixGroup')(self.db)
5616 pg.populate(parent=group)
5617 try:
5618 pg.write_db()
5619 except self.db.DatabaseError, m:
5620 raise CerebrumError, "Database error: %s" % m
5621 # 3. make user the owner of the group so he can administer it
5622 op_set = BofhdAuthOpSet(self.db)
5623 op_set.find_by_name(cereconf.BOFHD_AUTH_GROUPMODERATOR)
5624 op_target = BofhdAuthOpTarget(self.db)
5625 op_target.populate(group.entity_id, 'group')
5626 op_target.write_db()
5627 role = BofhdAuthRole(self.db)
5628 role.grant_auth(acc.entity_id, op_set.op_set_id, op_target.op_target_id)
5629 # 4. make user a member of his personal group
5630 self._group_add(None, uname, uname, member_type="account")
5631 # 5. make this group the primary group
5632 acc.gid_id = group.entity_id
5633 acc.write_db()
5634 # 6. add spreads corresponding to its owning user
5635 self._spread_sync_group(acc, group)
5636 # 7. give personal group a trait
5637 if hasattr(self.const, 'trait_personal_dfg'):
5638 pg.populate_trait(self.const.trait_personal_dfg,
5639 target_id=acc.entity_id)
5640 pg.write_db()
5641 return {'group_id': int(pg.posix_gid)}
5642
5643 # group posix_create
5644 all_commands['group_promote_posix'] = Command(
5645 ("group", "promote_posix"), GroupName(),
5646 SimpleString(help_ref="string_description", optional=True),
5647 fs=FormatSuggestion("Group promoted to PosixGroup, posix gid: %i",
5648 ("group_id",)), perm_filter='can_create_group')
5649 def group_promote_posix(self, operator, group, description=None):
5650 self.ba.can_create_group(operator.get_entity_id())
5651 is_posix = False
5652 try:
5653 self._get_group(group, grtype="PosixGroup")
5654 is_posix = True
5655 except CerebrumError:
5656 pass
5657 if is_posix:
5658 raise CerebrumError("%s is already a PosixGroup" % group)
5659
5660 group=self._get_group(group)
5661 pg = Utils.Factory.get('PosixGroup')(self.db)
5662 pg.populate(parent=group)
5663 try:
5664 pg.write_db()
5665 except self.db.DatabaseError, m:
5666 raise CerebrumError, "Database error: %s" % m
5667 return {'group_id': int(pg.posix_gid)}
5668
5669 # group posix_demote
5670 all_commands['group_demote_posix'] = Command(
5671 ("group", "demote_posix"), GroupName(), perm_filter='can_delete_group')
5672
5673 def group_demote_posix(self, operator, group):
5674 try:
5675 grp = self._get_group(group, grtype="PosixGroup")
5676 except self.db.DatabaseError, msg:
5677 if "posix_user_gid" in str(msg):
5678 raise CerebrumError(
5679 ("Assigned as primary group for posix user(s). "
5680 "Use 'group list %s'") % grp.group_name)
5681 raise
5682
5683 self.ba.can_delete_group(operator.get_entity_id(), grp)
5684 grp.demote_posix()
5685
5686 return "OK, demoted '%s'" % group
5687
5688 # group search
5689 all_commands['group_search'] = Command(
5690 ("group", "search"), SimpleString(help_ref="string_group_filter"),
5691 fs=FormatSuggestion("%8i %-16s %s", ("id", "name", "desc"),
5692 hdr="%8s %-16s %s" % ("Id", "Name", "Description")),
5693 perm_filter='can_search_group')
5694 def group_search(self, operator, filter=""):
5695 self.ba.can_search_group(operator.get_entity_id())
5696 group = self.Group_class(self.db)
5697 if filter == "":
5698 raise CerebrumError, "No filter specified"
5699 filters = {'name': None,
5700 'desc': None,
5701 'spread': None,
5702 'expired': "no"}
5703 rules = filter.split(",")
5704 for rule in rules:
5705 if rule.count(":"):
5706 filter_type, pattern = rule.split(":", 1)
5707 else:
5708 filter_type = 'name'
5709 pattern = rule
5710 if filter_type not in filters:
5711 raise CerebrumError, "Unknown filter type: %s" % filter_type
5712 filters[filter_type] = pattern
5713 if filters['name'] == '*' and len(rules) == 1:
5714 raise CerebrumError, "Please provide a more specific filter"
5715 # remap code_str to the actual constant object (the API requires it)
5716 if filters['spread']:
5717 filters['spread'] = self._get_constant(self.const.Spread,
5718 filters["spread"])
5719 filter_expired = not self._get_boolean(filters['expired'])
5720 ret = []
5721 for r in group.search(spread=filters['spread'],
5722 name=filters['name'],
5723 description=filters['desc'],
5724 filter_expired=filter_expired):
5725 ret.append({'id': r['group_id'],
5726 'name': r['name'],
5727 'desc': r['description'],
5728 })
5729 return ret
5730
5731 # group set_description
5732 all_commands['group_set_description'] = Command(
5733 ("group", "set_description"),
5734 GroupName(), SimpleString(help_ref="string_description"),
5735 perm_filter='can_alter_group')
5736 def group_set_description(self, operator, group, description):
5737 grp = self._get_group(group)
5738 self.ba.can_alter_group(operator.get_entity_id(), grp)
5739 grp.description = description
5740 grp.write_db()
5741 return "OK, description for group '%s' updated" % group
5742
5743 # exchange-relatert-jazz
5744 # set display name, only for distribution groups and roomlists
5745 # for the time being, but may be interesting to use for other
5746 # groups as well
5747 all_commands['group_set_displayname'] = Command(
5748 ("group", 'set_display_name'),
5749 GroupName(help_ref="group_name"),
5750 SimpleString(help_ref="group_disp_name"),
5751 SimpleString(help_ref='display_name_language', default='nb'),
5752 perm_filter="is_postmaster")
5753 def group_set_displayname(self, operator, gname, disp_name, name_lang):
5754 # if this methos is to be made generic use
5755 # _get_group(grptype="Group")
5756 if not self.ba.is_postmaster(operator.get_entity_id()):
5757 raise PermissionDenied('No access to group')
5758 name_variant = self.const.dl_group_displ_name
5759 self._set_display_name(gname, disp_name, name_variant, name_lang)
5760 return "Registered display name %s for %s" % (disp_name, gname)
5761
5762 # helper method, will use in distgroup_ and roomlist_create
5763 # as they both require sett display_name
5764 def _set_display_name(self, gname, disp_name, name_var, name_lang):
5765 # if this method is to be of generic use the name variant must
5766 # be made into a parameter. it may be advisable to change
5767 # dl_group_displ_name into a more generic group_display_name
5768 # value in the future
5769 group = self._get_group(gname, grtype="DistributionGroup")
5770 if name_lang in self.language_codes:
5771 name_lang = int(_LanguageCode(name_lang))
5772 else:
5773 return "Could not set display name, invalid language code"
5774 group.add_name_with_language(name_var, name_lang,
5775 disp_name)
5776 group.write_db()
5777
5778 # group set_expire
5779 all_commands['group_set_expire'] = Command(
5780 ("group", "set_expire"), GroupName(), Date(), perm_filter='can_delete_group')
5781 def group_set_expire(self, operator, group, expire):
5782 grp = self._get_group(group)
5783 self.ba.can_delete_group(operator.get_entity_id(), grp)
5784 grp.expire_date = self._parse_date(expire)
5785 grp.write_db()
5786 return "OK, set expire-date for '%s'" % group
5787
5788 # group set_visibility
5789 all_commands['group_set_visibility'] = Command(
5790 ("group", "set_visibility"), GroupName(), GroupVisibility(),
5791 perm_filter='can_delete_group')
5792 def group_set_visibility(self, operator, group, visibility):
5793 grp = self._get_group(group)
5794 self.ba.can_delete_group(operator.get_entity_id(), grp)
5795 grp.visibility = self._get_constant(self.const.GroupVisibility,
5796 visibility, "visibility")
5797 grp.write_db()
5798 return "OK, set visibility for '%s'" % group
5799
5800 # group memberships
5801 all_commands['group_memberships'] = Command(
5802 ('group', 'memberships'), EntityType(default="account"),
5803 Id(), Spread(optional=True, help_ref='spread_filter'),
5804 fs=FormatSuggestion(
5805 "%-9s %-18s", ("memberop", "group"),
5806 hdr="%-9s %-18s" % ("Operation", "Group")))
5807 def group_memberships(self, operator, entity_type, id,
5808 spread=None):
5809 entity = self._get_entity(entity_type, id)
5810 group = self.Group_class(self.db)
5811 co = self.const
5812 if spread is not None:
5813 spread = self._get_constant(self.const.Spread, spread, "spread")
5814 ret = []
5815 for row in group.search(member_id=entity.entity_id, spread=spread):
5816 ret.append({'memberop': str(co.group_memberop_union),
5817 'entity_id': row["group_id"],
5818 'group': row["name"],
5819 'description': row["description"],
5820 })
5821 ret.sort(lambda a,b: cmp(a['group'], b['group']))
5822 return ret
5823
5824 #
5825 # misc commands
5826 #
5827
5828 # misc affiliations
5829 all_commands['misc_affiliations'] = Command(
5830 ("misc", "affiliations"),
5831 fs=FormatSuggestion("%-14s %-14s %s", ('aff', 'status', 'desc'),
5832 hdr="%-14s %-14s %s" % ('Affiliation', 'Status',
5833 'Description')))
5834 def misc_affiliations(self, operator):
5835 tmp = {}
5836 duplicate_check_list = list()
5837 for co in self.const.fetch_constants(self.const.PersonAffStatus):
5838 aff = str(co.affiliation)
5839 if aff not in tmp:
5840 tmp[aff] = [{'aff': aff,
5841 'status': '',
5842 'desc': co.affiliation.description}]
5843 status = str(co._get_status())
5844 if (aff, status) in duplicate_check_list:
5845 continue
5846 tmp[aff].append({'aff': '',
5847 'status': status,
5848 'desc': co.description})
5849 duplicate_check_list.append((aff, status))
5850 # fetch_constants returns a list sorted according to the name
5851 # of the constant. Since the name of the constant and the
5852 # affiliation status usually are kept related, the list for
5853 # each affiliation will tend to be sorted as well. Not so for
5854 # the affiliations themselves.
5855 keys = tmp.keys()
5856 keys.sort()
5857 ret = []
5858 for k in keys:
5859 for r in tmp[k]:
5860 ret.append(r)
5861 return ret
5862
5863 all_commands['misc_change_request'] = Command(
5864 ("misc", "change_request"),
5865 Id(help_ref="id:request_id"), DateTimeString())
5866
5867 def misc_change_request(self, operator, request_id, datetime):
5868 if not request_id:
5869 raise CerebrumError('Request id required')
5870 if not datetime:
5871 raise CerebrumError('Date required')
5872 datetime = self._parse_date(datetime)
5873 br = BofhdRequests(self.db, self.const)
5874 old_req = br.get_requests(request_id=request_id)
5875 if not old_req:
5876 raise CerebrumError("There is no request with id=%s" % request_id)
5877 else:
5878 # If there is anything, it's at most one
5879 old_req = old_req[0]
5880 # If you are allowed to cancel a request, you can change it :)
5881 self.ba.can_cancel_request(operator.get_entity_id(), request_id)
5882 br.delete_request(request_id=request_id)
5883 br.add_request(operator.get_entity_id(), datetime,
5884 old_req['operation'], old_req['entity_id'],
5885 old_req['destination_id'],
5886 old_req['state_data'])
5887 return "OK, altered request %s" % request_id
5888
5889 # misc check_password
5890 all_commands['misc_check_password'] = Command(
5891 ("misc", "check_password"), AccountPassword())
5892 def misc_check_password(self, operator, password):
5893 ac = self.Account_class(self.db)
5894 try:
5895 check_password(password, ac, structured=False)
5896 except RigidPasswordNotGoodEnough as e:
5897 # tragically converting utf-8 -> unicode -> latin1
5898 # since bofh still speaks latin1
5899 raise CerebrumError('Bad password: {err_msg}'.format(
5900 err_msg=str(e).decode('utf-8').encode('latin-1')))
5901 except PhrasePasswordNotGoodEnough as e:
5902 raise CerebrumError('Bad passphrase: {err_msg}'.format(
5903 err_msg=str(e).decode('utf-8').encode('latin-1')))
5904 except PasswordNotGoodEnough as e:
5905 # should be used for a default (no style) message
5906 # used for backward compatibility paranoia reasons here
5907 raise CerebrumError('Bad password: {err_msg}'.format(err_msg=e))
5908 crypt = ac.encrypt_password(self.const.Authentication("crypt3-DES"),
5909 password)
5910 md5 = ac.encrypt_password(self.const.Authentication("MD5-crypt"),
5911 password)
5912 sha256 = ac.encrypt_password(self.const.auth_type_sha256_crypt, password)
5913 sha512 = ac.encrypt_password(self.const.auth_type_sha512_crypt, password)
5914 return ("OK.\n crypt3-DES: %s\n MD5-crypt: %s\n" % (crypt, md5) +
5915 " SHA256-crypt: %s\n SHA512-crypt: %s" % (sha256, sha512))
5916
5917 # misc clear_passwords
5918 all_commands['misc_clear_passwords'] = Command(
5919 ("misc", "clear_passwords"), AccountName(optional=True))
5920 def misc_clear_passwords(self, operator, account_name=None):
5921 operator.clear_state(state_types=('new_account_passwd', 'user_passwd'))
5922 return "OK, passwords cleared"
5923
5924
5925 all_commands['misc_dadd'] = Command(
5926 ("misc", "dadd"), SimpleString(help_ref='string_host'), DiskId(),
5927 perm_filter='can_create_disk')
5928 def misc_dadd(self, operator, hostname, diskname):
5929 host = self._get_host(hostname)
5930 self.ba.can_create_disk(operator.get_entity_id(), host)
5931
5932 if not diskname.startswith("/"):
5933 raise CerebrumError("'%s' does not start with '/'" % diskname)
5934
5935 if cereconf.VALID_DISK_TOPLEVELS is not None:
5936 toplevel_mountpoint = diskname.split("/")[1]
5937 if toplevel_mountpoint not in cereconf.VALID_DISK_TOPLEVELS:
5938 raise CerebrumError("'%s' is not a valid toplevel mountpoint"
5939 " for disks" % toplevel_mountpoint)
5940
5941 disk = Utils.Factory.get('Disk')(self.db)
5942 disk.populate(host.entity_id, diskname, 'uit disk')
5943 try:
5944 disk.write_db()
5945 except self.db.DatabaseError, m:
5946 raise CerebrumError, "Database error: %s" % m
5947 if len(diskname.split("/")) != 4:
5948 return "OK. Warning: disk did not follow expected pattern."
5949 return "OK, added disk '%s' at %s" % (diskname, hostname)
5950
5951
5952 all_commands['misc_samba_mount'] = Command(
5953 ("misc", "samba_mount"), DiskId(),DiskId())
5954 def misc_samba_mount(self, operator, hostname, mountname):
5955 if not self.ba.is_superuser(operator.get_entity_id()):
5956 raise PermissionDenied("Currently limited to superusers")
5957 from Cerebrum.modules import MountHost
5958 mount_host = MountHost.MountHost(self.db)
5959
5960 if hostname == 'delete':
5961 try:
5962 host = self._get_host(mountname)
5963 mount_host.find(host.entity_id)
5964 mount_host.delete_mount()
5965 return "Deleted %s from mount_host" % host.name
5966
5967 except Errors.NotFoundError:
5968 raise CerebrumError, "Unknown mount_host: %s" % host.name
5969
5970 elif hostname == 'list':
5971 if mountname == 'all':
5972 ename = Entity.EntityName(self.db)
5973 list_all = "%-16s%-16s\n" % ("host_name", "mount_name")
5974 for line in mount_host.list_all():
5975 m_host_name = self._get_host(int(line['mount_host_id']))
5976 list_all = "%s%-16s%-16s\n" % (list_all,
5977 m_host_name.name, line['mount_name'])
5978 return list_all
5979 else:
5980 host = self._get_host(mountname)
5981 try:
5982 mount_host.find(host.entity_id)
5983 return "%s -> %s" % (mountname, mount_host.mount_name)
5984 except Errors.NotFoundError:
5985 raise CerebrumError, "Unknown mount_host: %s" % host.name
5986
5987 else:
5988 host = self._get_host(hostname)
5989 m_host = self._get_host(mountname)
5990 try:
5991 mount_host.find(host.entity_id)
5992 mount_host.mount_name = m_host.name
5993 mount_host.host_id = m_host.entity_id
5994
5995 except Errors.NotFoundError:
5996 mount_host.populate(host.entity_id,
5997 m_host.entity_id, m_host.name)
5998
5999 mount_host.write_db()
6000 return "Updated samba mountpoint: %s on %s" % (m_host.name,
6001 host.name)
6002
6003
6004 # misc dls is deprecated, and can probably be removed without
6005 # anyone complaining much.
6006 all_commands['misc_dls'] = Command(
6007 ("misc", "dls"), SimpleString(help_ref='string_host'),
6008 fs=FormatSuggestion("%-8i %-8i %s", ("disk_id", "host_id", "path",),
6009 hdr="DiskId HostId Path"))
6010 def misc_dls(self, operator, hostname):
6011 return self.disk_list(operator, hostname)
6012
6013 all_commands['disk_list'] = Command(
6014 ("disk", "list"), SimpleString(help_ref='string_host'),
6015 fs=FormatSuggestion("%-13s %11s %s",
6016 ("hostname", "pretty_quota", "path",),
6017 hdr="Hostname Default quota Path"))
6018 def disk_list(self, operator, hostname):
6019 host = self._get_host(hostname)
6020 disks = {}
6021 disk = Utils.Factory.get('Disk')(self.db)
6022 hquota = host.get_trait(self.const.trait_host_disk_quota)
6023 if hquota:
6024 hquota = hquota['numval']
6025 for row in disk.list(host.host_id):
6026 disk.clear()
6027 disk.find(row['disk_id'])
6028 dquota = disk.get_trait(self.const.trait_disk_quota)
6029 if dquota is None:
6030 def_quota = None
6031 pretty_quota = '<none>'
6032 else:
6033 if dquota['numval'] is None:
6034 def_quota = hquota
6035 if hquota is None:
6036 pretty_quota = '(no default)'
6037 else:
6038 pretty_quota = '(%d MiB)' % def_quota
6039 else:
6040 def_quota = dquota['numval']
6041 pretty_quota = '%d MiB' % def_quota
6042 disks[row['disk_id']] = {'disk_id': row['disk_id'],
6043 'host_id': row['host_id'],
6044 'hostname': hostname,
6045 'def_quota': def_quota,
6046 'pretty_quota': pretty_quota,
6047 'path': row['path']}
6048 disklist = disks.keys()
6049 disklist.sort(lambda x, y: cmp(disks[x]['path'], disks[y]['path']))
6050 ret = []
6051 for d in disklist:
6052 ret.append(disks[d])
6053 return ret
6054
6055 all_commands['disk_quota'] = Command(
6056 ("disk", "quota"), SimpleString(help_ref='string_host'), DiskId(),
6057 SimpleString(help_ref='disk_quota_set'),
6058 perm_filter='can_set_disk_default_quota')
6059 def disk_quota(self, operator, hostname, diskname, quota):
6060 host = self._get_host(hostname)
6061 disk = self._get_disk(diskname, host_id=host.entity_id)[0]
6062 self.ba.can_set_disk_default_quota(operator.get_entity_id(),
6063 host=host, disk=disk)
6064 old = disk.get_trait(self.const.trait_disk_quota)
6065 if quota.lower() == 'none':
6066 if old:
6067 disk.delete_trait(self.const.trait_disk_quota)
6068 return "OK, no quotas on %s" % diskname
6069 elif quota.lower() == 'default':
6070 disk.populate_trait(self.const.trait_disk_quota,
6071 numval=None)
6072 disk.write_db()
6073 return "OK, using host default on %s" % diskname
6074 elif quota.isdigit():
6075 disk.populate_trait(self.const.trait_disk_quota,
6076 numval=int(quota))
6077 disk.write_db()
6078 return "OK, default quota on %s is %d" % (diskname, int(quota))
6079 else:
6080 raise CerebrumError, "Invalid quota value '%s'" % quota
6081
6082 all_commands['misc_drem'] = Command(
6083 ("misc", "drem"), SimpleString(help_ref='string_host'), DiskId(),
6084 perm_filter='can_remove_disk')
6085 def misc_drem(self, operator, hostname, diskname):
6086 host = self._get_host(hostname)
6087 self.ba.can_remove_disk(operator.get_entity_id(), host)
6088 disk = self._get_disk(diskname, host_id=host.entity_id)[0]
6089 # FIXME: We assume that all destination_ids are entities,
6090 # which would ensure that the disk_id number can't represent a
6091 # different kind of entity. The database does not constrain
6092 # this, however.
6093 br = BofhdRequests(self.db, self.const)
6094 if br.get_requests(destination_id=disk.entity_id):
6095 raise CerebrumError, ("There are pending requests. Use "+
6096 "'misc list_requests disk %s' to view "+
6097 "them.") % diskname
6098 account = self.Account_class(self.db)
6099 for row in account.list_account_home(disk_id=disk.entity_id,
6100 filter_expired=False):
6101 if row['disk_id'] is None:
6102 continue
6103 if row['status'] == int(self.const.home_status_on_disk):
6104 raise CerebrumError, ("One or more users still on disk " +
6105 "(e.g. %s)" % row['entity_name'])
6106 account.clear()
6107 account.find(row['account_id'])
6108 ah = account.get_home(row['home_spread'])
6109 account.set_homedir(
6110 current_id=ah['homedir_id'], disk_id=None,
6111 home=account.resolve_homedir(disk_path=row['path'], home=row['home']))
6112 self._remove_auth_target("disk", disk.entity_id)
6113 try:
6114 disk.delete()
6115 except self.db.DatabaseError, m:
6116 raise CerebrumError, "Database error: %s" % m
6117 return "OK, %s deleted" % diskname
6118
6119 all_commands['misc_hadd'] = Command(
6120 ("misc", "hadd"), SimpleString(help_ref='string_host'),
6121 perm_filter='can_create_host')
6122 def misc_hadd(self, operator, hostname):
6123 self.ba.can_create_host(operator.get_entity_id())
6124 host = Utils.Factory.get('Host')(self.db)
6125 host.populate(hostname, 'uit host')
6126 try:
6127 host.write_db()
6128 except self.db.DatabaseError, m:
6129 raise CerebrumError, "Database error: %s" % m
6130 return "OK, added host '%s'" % hostname
6131
6132 all_commands['misc_hrem'] = Command(
6133 ("misc", "hrem"), SimpleString(help_ref='string_host'),
6134 perm_filter='can_remove_host')
6135 def misc_hrem(self, operator, hostname):
6136 self.ba.can_remove_host(operator.get_entity_id())
6137 host = self._get_host(hostname)
6138 self._remove_auth_target("host", host.host_id)
6139 try:
6140 host.delete()
6141 except self.db.DatabaseError, m:
6142 raise CerebrumError, "Database error: %s" % m
6143 return "OK, %s deleted" % hostname
6144
6145 # See hack in list_command
6146 def host_info(self, operator, hostname, policy=False):
6147 ret = []
6148 # More hacks follow.
6149 # Call the DNS module's host_info command for data:
6150 dns_err = None
6151 try:
6152 from Cerebrum.modules.dns.bofhd_dns_cmds import BofhdExtension as DnsCmds
6153 from Cerebrum.modules.dns import Utils as DnsUtils
6154 from Cerebrum.modules.dns.bofhd_dns_utils import DnsBofhdUtils
6155 zone = self.const.DnsZone("uit")
6156 # Avoid Python's type checking. The BofhdExtension this
6157 # "self" is an instance of is different from the
6158 # BofhdExtension host_info expects. By using a function
6159 # reference, it suffices that "self" we pass in supports
6160 # the same API.
6161 host_info = DnsCmds.__dict__.get('host_info')
6162 # To support the API, we add some stuff to this object.
6163 # Ugh. Better hope this doesn't stomp on anything.
6164 self._find = DnsUtils.Find(self.db, zone)
6165 self.mb_utils = DnsBofhdUtils(self.db, self.logger, zone)
6166 self.dns_parser = DnsUtils.DnsParser(self.db, zone)
6167 ret = host_info(self, operator, hostname, policy=policy)
6168 except CerebrumError, dns_err:
6169 # Even though the DNS module doesn't recognise the host, the
6170 # standard host_info could still have some info. We should therefore
6171 # continue and see if we could get more info.
6172 pass
6173 # Other exceptions are faults and should cause trouble
6174 # TODO: make it possible to check if the DNS module are in use by the
6175 # active instance.
6176
6177 try:
6178 host = self._get_host(hostname)
6179 except CerebrumError:
6180 # Only return data from the DNS module
6181 if dns_err is not None:
6182 raise dns_err
6183 return ret
6184 ret = [{'hostname': hostname,
6185 'desc': host.description}] + ret
6186 hquota = host.get_trait(self.const.trait_host_disk_quota)
6187 if hquota and hquota['numval']:
6188 ret.append({'def_disk_quota': hquota['numval']})
6189 return ret
6190
6191 all_commands['host_disk_quota'] = Command(
6192 ("host", "disk_quota"), SimpleString(help_ref='string_host'),
6193 SimpleString(help_ref='disk_quota_set'),
6194 perm_filter='can_set_disk_default_quota')
6195 def host_disk_quota(self, operator, hostname, quota):
6196 host = self._get_host(hostname)
6197 self.ba.can_set_disk_default_quota(operator.get_entity_id(),
6198 host=host)
6199 old = host.get_trait(self.const.trait_host_disk_quota)
6200 if (quota.lower() == 'none' or quota.lower() == 'default' or
6201 (quota.isdigit() and int(quota) == 0)):
6202 # "default" doesn't make much sense, but the help text
6203 # says it's a valid value.
6204 if old:
6205 disk.delete_trait(self.const.trait_disk_quota)
6206 return "OK, no default quota on %s" % hostname
6207 elif quota.isdigit() and int(quota) > 0:
6208 host.populate_trait(self.const.trait_host_disk_quota,
6209 numval=int(quota))
6210 host.write_db()
6211 return "OK, default quota on %s is %d" % (hostname, int(quota))
6212 else:
6213 raise CerebrumError("Invalid quota value '%s'" % quota)
6214 pass
6215
6216 def _remove_auth_target(self, target_type, target_id):
6217 """This function should be used whenever a potential target
6218 for authorisation is deleted.
6219 """
6220 ar = BofhdAuthRole(self.db)
6221 aot = BofhdAuthOpTarget(self.db)
6222 for r in aot.list(entity_id=target_id, target_type=target_type):
6223 aot.clear()
6224 aot.find(r['op_target_id'])
6225 # We remove all auth_role entries first so that there
6226 # are no references to this op_target_id, just in case
6227 # someone adds a foreign key constraint later.
6228 for role in ar.list(op_target_id=r["op_target_id"]):
6229 ar.revoke_auth(role['entity_id'],
6230 role['op_set_id'],
6231 r['op_target_id'])
6232 aot.delete()
6233
6234 def _remove_auth_role(self, entity_id):
6235 """This function should be used whenever a potentially
6236 authorised entity is deleted.
6237 """
6238 ar = BofhdAuthRole(self.db)
6239 aot = BofhdAuthOpTarget(self.db)
6240 for r in ar.list(entity_id):
6241 ar.revoke_auth(entity_id, r['op_set_id'], r['op_target_id'])
6242 # Also remove targets if this was the last reference from
6243 # auth_role.
6244 remaining = ar.list(op_target_id=r['op_target_id'])
6245 if len(remaining) == 0:
6246 aot.clear()
6247 aot.find(r['op_target_id'])
6248 aot.delete()
6249
6250 all_commands['misc_list_passwords'] = Command(
6251 ("misc", "list_passwords"),
6252 fs=FormatSuggestion(
6253 "%-8s %-20s %s", ("account_id", "operation", "password"),
6254 hdr="%-8s %-20s %s" % ("Id", "Operation", "Password")))
6255
6256 def misc_list_passwords(self, operator, *args):
6257 u""" List passwords in cache. """
6258 # NOTE: We keep the *args argument for backwards compability.
6259 cache = self._get_cached_passwords(operator)
6260 if not cache:
6261 raise CerebrumError("No passwords in session")
6262 return cache
6263
6264 all_commands['misc_list_bofhd_request_types'] = Command(
6265 ("misc", "list_bofhd_request_types"),
6266 fs=FormatSuggestion(
6267 "%-20s %s", ("code_str", "description"),
6268 hdr="%-20s %s" % ("Code", "Description")))
6269
6270 def misc_list_bofhd_request_types(self, operator):
6271 br = BofhdRequests(self.db, self.const)
6272 result = []
6273 for row in br.get_operations():
6274 result.append({"code_str": row["code_str"].lstrip("br_"),
6275 "description": row["description"]})
6276 return result
6277
6278 all_commands['misc_list_requests'] = Command(
6279 ("misc", "list_requests"),
6280 SimpleString(help_ref='string_bofh_request_search_by',
6281 default='requestee'),
6282 SimpleString(help_ref='string_bofh_request_target',
6283 default='<me>'),
6284 fs=FormatSuggestion(
6285 "%-7i %-10s %-16s %-16s %-10s %-20s %s",
6286 ("id", "requestee", format_time("when"), "op", "entity",
6287 "destination", "args"),
6288 hdr="%-7s %-10s %-16s %-16s %-10s %-20s %s" % (
6289 "Id", "Requestee", "When", "Op", "Entity", "Destination",
6290 "Arguments")))
6291
6292 def misc_list_requests(self, operator, search_by, destination):
6293 br = BofhdRequests(self.db, self.const)
6294 ret = []
6295
6296 if destination == '<me>':
6297 destination = self._get_account(operator.get_entity_id(), idtype='id')
6298 destination = destination.account_name
6299 if search_by == 'requestee':
6300 account = self._get_account(destination)
6301 rows = br.get_requests(operator_id=account.entity_id, given=True)
6302 elif search_by == 'operation':
6303 try:
6304 destination = int(self.const.BofhdRequestOp('br_'+destination))
6305 except Errors.NotFoundError:
6306 raise CerebrumError("Unknown request operation %s" % destination)
6307 rows = br.get_requests(operation=destination)
6308 elif search_by == 'disk':
6309 disk_id = self._get_disk(destination)[1]
6310 rows = br.get_requests(destination_id=disk_id)
6311 elif search_by == 'account':
6312 account = self._get_account(destination)
6313 rows = br.get_requests(entity_id=account.entity_id)
6314 else:
6315 raise CerebrumError("Unknown search_by criteria")
6316
6317 for r in rows:
6318 op = self.const.BofhdRequestOp(r['operation'])
6319 dest = None
6320 ent_name = None
6321 if op in (self.const.bofh_move_user, self.const.bofh_move_request,
6322 self.const.bofh_move_user_now):
6323 disk = self._get_disk(r['destination_id'])[0]
6324 dest = disk.path
6325 elif op in (self.const.bofh_move_give,):
6326 dest = self._get_entity_name(r['destination_id'],
6327 self.const.entity_group)
6328 elif op in (self.const.bofh_email_create,
6329 self.const.bofh_email_move,
6330 self.const.bofh_email_delete):
6331 dest = self._get_entity_name(r['destination_id'],
6332 self.const.entity_host)
6333 elif op in (self.const.bofh_sympa_create,
6334 self.const.bofh_sympa_remove):
6335 ea = Email.EmailAddress(self.db)
6336 if r['destination_id'] is not None:
6337 ea.find(r['destination_id'])
6338 dest = ea.get_address()
6339 ea.clear()
6340 try:
6341 ea.find(r['entity_id'])
6342 except Errors.NotFoundError:
6343 ent_name = "<not found>"
6344 else:
6345 ent_name = ea.get_address()
6346 if ent_name is None:
6347 ent_name = self._get_entity_name(r['entity_id'],
6348 self.const.entity_account)
6349 if r['requestee_id'] is None:
6350 requestee = ''
6351 else:
6352 requestee = self._get_entity_name(r['requestee_id'],
6353 self.const.entity_account)
6354 ret.append({'when': r['run_at'],
6355 'requestee': requestee,
6356 'op': str(op),
6357 'entity': ent_name,
6358 'destination': dest,
6359 'args': r['state_data'],
6360 'id': r['request_id']
6361 })
6362 ret.sort(lambda a,b: cmp(a['id'], b['id']))
6363 return ret
6364
6365 all_commands['misc_cancel_request'] = Command(
6366 ("misc", "cancel_request"),
6367 SimpleString(help_ref='id:request_id'))
6368 def misc_cancel_request(self, operator, req):
6369 if req.isdigit():
6370 req_id = int(req)
6371 else:
6372 raise CerebrumError, "Request-ID must be a number"
6373 br = BofhdRequests(self.db, self.const)
6374 if not br.get_requests(request_id=req_id):
6375 raise CerebrumError, "Request ID %d not found" % req_id
6376 self.ba.can_cancel_request(operator.get_entity_id(), req_id)
6377 br.delete_request(request_id=req_id)
6378 return "OK, %s canceled" % req
6379
6380 all_commands['misc_reload'] = Command(
6381 ("misc", "reload"),
6382 perm_filter='is_superuser')
6383 def misc_reload(self, operator):
6384 if not self.ba.is_superuser(operator.get_entity_id()):
6385 raise PermissionDenied("Currently limited to superusers")
6386 self.server.read_config()
6387 return "OK, server-config reloaded"
6388
6389 # ou search <pattern> <language> <spread_filter>
6390 all_commands['ou_search'] = Command(
6391 ("ou", "search"),
6392 SimpleString(help_ref='ou_search_pattern'),
6393 SimpleString(help_ref='ou_search_language', optional=True),
6394 Spread(help_ref='spread_filter', optional=True),
6395 fs=FormatSuggestion([
6396 (" %06s %s", ('stedkode', 'name'))
6397 ],
6398 hdr="Stedkode Organizational unit"))
6399 def ou_search(self, operator, pattern, language='nb', spread_filter=None):
6400 if len(pattern) == 0:
6401 pattern = '%' # No search pattern? Get everything!
6402
6403 try:
6404 language = int(self.const.LanguageCode(language))
6405 except Errors.NotFoundError:
6406 raise CerebrumError, 'Unknown language "%s", try "nb" or "en"' % language
6407
6408 output = []
6409 ou = Utils.Factory.get('OU')(self.db)
6410
6411 if re.match(r'[0-9]{1,6}$', pattern):
6412 fak = [ pattern[0:2] ]
6413 inst = [ pattern[2:4] ]
6414 avd = [ pattern[4:6] ]
6415
6416 if len(fak[0]) == 1:
6417 fak = [ int(fak[0]) * 10 + x for x in range(10) ]
6418 if len(inst[0]) == 1:
6419 inst = [ int(inst[0]) * 10 + x for x in range(10) ]
6420 if len(avd[0]) == 1:
6421 avd = [ int(avd[0]) * 10 + x for x in range(10) ]
6422
6423 # the following loop may look scary, but we will never
6424 # call get_stedkoder() more than 10 times.
6425 for f in fak:
6426 for i in inst:
6427 if i == '':
6428 i = None
6429 for a in avd:
6430 if a == '':
6431 a = None
6432 for r in ou.get_stedkoder(fakultet=f, institutt=i,
6433 avdeling=a):
6434 ou.clear()
6435 ou.find(r['ou_id'])
6436
6437 if spread_filter:
6438 spread_filter_match = False
6439 for spread in ou.get_spread():
6440 if str(self.const.Spread(spread[0])).lower() == spread_filter.lower():
6441 spread_filter_match = True
6442 break
6443
6444 acronym = ou.get_name_with_language(
6445 name_variant=self.const.ou_name_acronym,
6446 name_language=language,
6447 default="")
6448 name = ou.get_name_with_language(
6449 name_variant=self.const.ou_name,
6450 name_language=language,
6451 default="")
6452
6453 if len(acronym) > 0:
6454 acronym = "(%s) " % acronym
6455
6456 if not spread_filter or (spread_filter and spread_filter_match):
6457 output.append({
6458 'stedkode': '%02d%02d%02d' % (ou.fakultet,
6459 ou.institutt,
6460 ou.avdeling),
6461 'name': "%s%s" % (acronym, name)
6462 })
6463 else:
6464 for r in ou.search_name_with_language(
6465 entity_type=self.const.entity_ou,
6466 name_language=language,
6467 name=pattern,
6468 exact_match=False):
6469 ou.clear()
6470 ou.find(r['entity_id'])
6471
6472 if spread_filter:
6473 spread_filter_match = False
6474 for spread in ou.get_spread():
6475 if str(self.const.Spread(spread[0])).lower() == spread_filter.lower():
6476 spread_filter_match = True
6477 break
6478
6479 acronym = ou.get_name_with_language(
6480 name_variant=self.const.ou_name_acronym,
6481 name_language=language,
6482 default="")
6483 name = ou.get_name_with_language(
6484 name_variant=self.const.ou_name,
6485 name_language=language,
6486 default="")
6487
6488 if len(acronym) > 0:
6489 acronym = "(%s) " % acronym
6490
6491 if not spread_filter or (spread_filter and spread_filter_match):
6492 output.append({
6493 'stedkode': '%02d%02d%02d' % (ou.fakultet,
6494 ou.institutt,
6495 ou.avdeling),
6496 'name': "%s%s" % (acronym, name)
6497 })
6498
6499 if len(output) == 0:
6500 if spread_filter:
6501 return 'No matches for "%s" with spread filter "%s"' % (pattern, spread_filter)
6502 return 'No matches for "%s"' % pattern
6503
6504 #removes duplicate results
6505 seen = set()
6506 output_nodupes = []
6507 for r in output:
6508 t = tuple(r.items())
6509 if t not in seen:
6510 seen.add(t)
6511 output_nodupes.append(r)
6512
6513 return output_nodupes
6514
6515 # ou info <stedkode/entity_id>
6516 all_commands['ou_info'] = Command(
6517 ("ou", "info"),
6518 OU(help_ref='ou_stedkode_or_id'),
6519 fs=FormatSuggestion([
6520 ("Stedkode: %s\n" +
6521 "Entity ID: %i\n" +
6522 "Name (nb): %s\n" +
6523 "Name (en): %s\n" +
6524 "Quarantines: %s\n" +
6525 "Spreads: %s",
6526 ('stedkode', 'entity_id', 'name_nb', 'name_en', 'quarantines',
6527 'spreads')),
6528 ("Contact: (%s) %s: %s",
6529 ('contact_source', 'contact_type', 'contact_value')),
6530 ("Address: (%s) %s: %s%s%s %s %s",
6531 ('address_source', 'address_type', 'address_text', 'address_po_box',
6532 'address_postal_number', 'address_city', 'address_country')),
6533 ("Email domain: affiliation %-7s @%s",
6534 ('email_affiliation', 'email_domain'))
6535 ]
6536 ))
6537 def ou_info(self, operator, target):
6538 output = []
6539
6540 ou = self.util.get_target(target, default_lookup='stedkode', restrict_to=['OU'])
6541
6542 acronym_nb = ou.get_name_with_language(
6543 name_variant=self.const.ou_name_acronym,
6544 name_language=self.const.language_nb,
6545 default="")
6546 fullname_nb = ou.get_name_with_language(
6547 name_variant=self.const.ou_name,
6548 name_language=self.const.language_nb,
6549 default="")
6550 acronym_en = ou.get_name_with_language(
6551 name_variant=self.const.ou_name_acronym,
6552 name_language=self.const.language_en,
6553 default="")
6554 fullname_en = ou.get_name_with_language(
6555 name_variant=self.const.ou_name,
6556 name_language=self.const.language_en,
6557 default="")
6558
6559 if len(acronym_nb) > 0:
6560 acronym_nb = "(%s) " % acronym_nb
6561
6562 if len(acronym_en) > 0:
6563 acronym_en = "(%s) " % acronym_en
6564
6565 quarantines = []
6566 for q in ou.get_entity_quarantine(only_active=True):
6567 quarantines.append(str(self.const.Quarantine(q['quarantine_type'])))
6568 if len(quarantines) == 0:
6569 quarantines = ['<none>']
6570
6571 spreads = []
6572 for s in ou.get_spread():
6573 spreads.append(str(self.const.Spread(s['spread'])))
6574 if len(spreads) == 0:
6575 spreads = ['<none>']
6576
6577 # To support OU objects without the mixin for stedkode:
6578 stedkode = '<Not set>'
6579 if hasattr(ou, 'fakultet'):
6580 stedkode = '%02d%02d%02d' % (ou.fakultet, ou.institutt, ou.avdeling)
6581
6582 output.append({
6583 'entity_id': ou.entity_id,
6584 'stedkode': stedkode,
6585 'name_nb': "%s%s" % (acronym_nb, fullname_nb),
6586 'name_en': "%s%s" % (acronym_en, fullname_en),
6587 'quarantines': ', '.join(quarantines),
6588 'spreads': ', '.join(spreads)
6589 })
6590
6591 for c in ou.get_contact_info():
6592 output.append({
6593 'contact_source': str(self.const.AuthoritativeSystem(c['source_system'])),
6594 'contact_type': str(self.const.ContactInfo(c['contact_type'])),
6595 'contact_value': c['contact_value']
6596 })
6597
6598 for a in ou.get_entity_address():
6599 if a['country'] is not None:
6600 a['country'] = ', ' + a['country']
6601 else:
6602 a['country'] = ''
6603
6604 if a['p_o_box'] is not None:
6605 a['p_o_box'] = "PO box %s, " % a['p_o_box']
6606 else:
6607 a['p_o_box'] = ''
6608
6609 if len(a['address_text']) > 0:
6610 a['address_text'] += ', '
6611
6612 output.append({
6613 'address_source': str(self.const.AuthoritativeSystem(a['source_system'])),
6614 'address_type': str(self.const.Address(a['address_type'])),
6615 'address_text': a['address_text'].replace("\n", ', '),
6616 'address_po_box': a['p_o_box'],
6617 'address_city': a['city'],
6618 'address_postal_number': a['postal_number'],
6619 'address_country': a['country']
6620 })
6621
6622 try:
6623 meta = Metainfo.Metainfo(self.db)
6624 email_info = meta.get_metainfo('sqlmodule_email')
6625 except Errors.NotFoundError:
6626 email_info = None
6627 if email_info:
6628 eed = Email.EntityEmailDomain(self.db)
6629 try:
6630 eed.find(ou.entity_id)
6631 except Errors.NotFoundError:
6632 pass
6633 ed = Email.EmailDomain(self.db)
6634 for r in eed.list_affiliations():
6635 affname = "<any>"
6636 if r['affiliation']:
6637 affname = str(self.const.PersonAffiliation(r['affiliation']))
6638 ed.clear()
6639 ed.find(r['domain_id'])
6640
6641 output.append({'email_affiliation': affname,
6642 'email_domain': ed.email_domain_name})
6643
6644 return output
6645
6646 # ou tree <stedkode/entity_id> <perspective> <language>
6647 all_commands['ou_tree'] = Command(
6648 ("ou", "tree"),
6649 OU(help_ref='ou_stedkode_or_id'),
6650 SimpleString(help_ref='ou_perspective', optional=True),
6651 SimpleString(help_ref='ou_search_language', optional=True),
6652 fs=FormatSuggestion([
6653 ("%s%s %s",
6654 ('indent', 'stedkode', 'name'))
6655 ]
6656 ))
6657 def ou_tree(self, operator, target, ou_perspective=None, language='nb'):
6658 def _is_root(ou, perspective):
6659 if ou.get_parent(perspective) in (ou.entity_id, None):
6660 return True
6661 return False
6662
6663 co = self.const
6664
6665 try:
6666 language = int(co.LanguageCode(language))
6667 except Errors.NotFoundError:
6668 raise CerebrumError, 'Unknown language "%s", try "nb" or "en"' % language
6669
6670 output = []
6671
6672 perspective = None
6673 if ou_perspective:
6674 perspective = co.human2constant(ou_perspective, co.OUPerspective)
6675 if not ou_perspective and 'perspective' in cereconf.LDAP_OU:
6676 perspective = co.human2constant(cereconf.LDAP_OU['perspective'], co.OUPerspective)
6677 if ou_perspective and not perspective:
6678 raise CerebrumError, 'No match for perspective "%s". Try one of: %s' % (
6679 ou_perspective,
6680 ", ".join(str(x) for x in co.fetch_constants(co.OUPerspective))
6681 )
6682 if not perspective:
6683 raise CerebrumError, "Unable to guess perspective. Please specify one of: %s" % (
6684 ", ".join(str(x) for x in co.fetch_constants(co.OUPerspective))
6685 )
6686
6687 target_ou = self.util.get_target(target, default_lookup='stedkode', restrict_to=['OU'])
6688 ou = Utils.Factory.get('OU')(self.db)
6689
6690 data = {
6691 'parents': [],
6692 'target': [target_ou.entity_id],
6693 'children': []
6694 }
6695
6696 prev_parent = None
6697
6698 try:
6699 while True:
6700 if prev_parent:
6701 ou.clear()
6702 ou.find(prev_parent)
6703
6704 if _is_root(ou, perspective):
6705 break
6706
6707 prev_parent = ou.get_parent(perspective)
6708 data['parents'].insert(0, prev_parent)
6709 else:
6710 if _is_root(target_ou, perspective):
6711 break
6712
6713 prev_parent = target_ou.get_parent(perspective)
6714 data['parents'].insert(0, prev_parent)
6715 except:
6716 raise CerebrumError, 'Error getting OU structure for %s. Is the OU valid?' % target
6717
6718 for c in target_ou.list_children(perspective):
6719 data['children'].append(c[0])
6720
6721 for d in data:
6722 if d is 'target':
6723 indent = '* ' + (len(data['parents']) -1) * ' '
6724 elif d is 'children':
6725 indent = (len(data['parents']) +1) * ' '
6726 if len(data['parents']) == 0:
6727 indent += ' '
6728
6729 for num, item in enumerate(data[d]):
6730 ou.clear()
6731 ou.find(item)
6732
6733 if d is 'parents':
6734 indent = num * ' '
6735
6736 output.append({
6737 'indent': indent,
6738 'stedkode': '%02d%02d%02d' % (ou.fakultet, ou.institutt, ou.avdeling),
6739 'name': ou.get_name_with_language(
6740 name_variant=co.ou_name,
6741 name_language=language,
6742 default="")
6743 })
6744
6745 return output
6746
6747
6748 # misc verify_password
6749 all_commands['misc_verify_password'] = Command(
6750 ("misc", "verify_password"), AccountName(), AccountPassword())
6751 def misc_verify_password(self, operator, accountname, password):
6752 ac = self._get_account(accountname)
6753 # Only people who can set the password are allowed to check it
6754 self.ba.can_set_password(operator.get_entity_id(), ac)
6755 if ac.verify_auth(password):
6756 return "Password is correct"
6757 ph = PasswordHistory(self.db)
6758 histhash = ph.encode_for_history(ac.account_name, password)
6759 for r in ph.get_history(ac.entity_id):
6760 if histhash == r['md5base64']:
6761 return ("The password is obsolete, it was set on %s" %
6762 r['set_at'])
6763 return "Incorrect password"
6764
6765
6766 #
6767 # perm commands
6768 #
6769
6770 # perm opset_list
6771 all_commands['perm_opset_list'] = Command(
6772 ("perm", "opset_list"),
6773 fs=FormatSuggestion("%-6i %s", ("id", "name"), hdr="Id Name"),
6774 perm_filter='is_superuser')
6775 def perm_opset_list(self, operator):
6776 if not self.ba.is_superuser(operator.get_entity_id()):
6777 raise PermissionDenied("Currently limited to superusers")
6778 aos = BofhdAuthOpSet(self.db)
6779 ret = []
6780 for r in aos.list():
6781 ret.append({'id': r['op_set_id'],
6782 'name': r['name']})
6783 return ret
6784
6785 # perm opset_show
6786 all_commands['perm_opset_show'] = Command(
6787 ("perm", "opset_show"), SimpleString(help_ref="string_op_set"),
6788 fs=FormatSuggestion("%-6i %-16s %s", ("op_id", "op", "attrs"),
6789 hdr="%-6s %-16s %s" % ("Id", "op", "Attributes")),
6790 perm_filter='is_superuser')
6791 def perm_opset_show(self, operator, name):
6792 if not self.ba.is_superuser(operator.get_entity_id()):
6793 raise PermissionDenied("Currently limited to superusers")
6794 aos = BofhdAuthOpSet(self.db)
6795 aos.find_by_name(name)
6796 ret = []
6797 for r in aos.list_operations():
6798 c = AuthConstants(int(r['op_code']))
6799 ret.append({'op': str(c),
6800 'op_id': r['op_id'],
6801 'attrs': ", ".join(
6802 ["%s" % r2['attr'] for r2 in aos.list_operation_attrs(r['op_id'])])})
6803 return ret
6804
6805 # perm target_list
6806 all_commands['perm_target_list'] = Command(
6807 ("perm", "target_list"), SimpleString(help_ref="string_perm_target"),
6808 Id(optional=True),
6809 fs=FormatSuggestion("%-8i %-15i %-10s %-18s %s",
6810 ("tgt_id", "entity_id", "target_type", "name", "attrs"),
6811 hdr="%-8s %-15s %-10s %-18s %s" % (
6812 "TargetId", "TargetEntityId", "TargetType", "TargetName", "Attrs")),
6813 perm_filter='is_superuser')
6814 def perm_target_list(self, operator, target_type, entity_id=None):
6815 if not self.ba.is_superuser(operator.get_entity_id()):
6816 raise PermissionDenied("Currently limited to superusers")
6817 aot = BofhdAuthOpTarget(self.db)
6818 ret = []
6819 if target_type.isdigit():
6820 rows = aot.list(target_id=target_type)
6821 else:
6822 rows = aot.list(target_type=target_type, entity_id=entity_id)
6823 for r in rows:
6824 if r['target_type'] == 'group':
6825 name = self._get_entity_name(r['entity_id'], self.const.entity_group)
6826 elif r['target_type'] == 'disk':
6827 name = self._get_entity_name(r['entity_id'], self.const.entity_disk)
6828 elif r['target_type'] == 'host':
6829 name = self._get_entity_name(r['entity_id'], self.const.entity_host)
6830 else:
6831 name = "unknown"
6832 ret.append({'tgt_id': r['op_target_id'],
6833 'entity_id': r['entity_id'],
6834 'name': name,
6835 'target_type': r['target_type'],
6836 'attrs': r['attr'] or '<none>'})
6837 return ret
6838
6839 # perm add_target
6840 all_commands['perm_add_target'] = Command(
6841 ("perm", "add_target"),
6842 SimpleString(help_ref="string_perm_target_type"), Id(),
6843 SimpleString(help_ref="string_attribute", optional=True),
6844 perm_filter='is_superuser')
6845 def perm_add_target(self, operator, target_type, entity_id, attr=None):
6846 if not self.ba.is_superuser(operator.get_entity_id()):
6847 raise PermissionDenied("Currently limited to superusers")
6848 if entity_id.isdigit():
6849 entity_id = int(entity_id)
6850 else:
6851 raise CerebrumError("Integer entity_id expected; got %r" %
6852 (entity_id,))
6853 aot = BofhdAuthOpTarget(self.db)
6854 aot.populate(entity_id, target_type, attr)
6855 aot.write_db()
6856 return "OK, target id=%d" % aot.op_target_id
6857
6858 # perm del_target
6859 all_commands['perm_del_target'] = Command(
6860 ("perm", "del_target"), Id(help_ref="id:op_target"),
6861 perm_filter='is_superuser')
6862 def perm_del_target(self, operator, op_target_id, attr):
6863 if not self.ba.is_superuser(operator.get_entity_id()):
6864 raise PermissionDenied("Currently limited to superusers")
6865 aot = BofhdAuthOpTarget(self.db)
6866 aot.find(op_target_id)
6867 aot.delete()
6868 return "OK, target %s, attr=%s deleted" % (op_target_id, attr)
6869
6870 # perm list
6871 all_commands['perm_list'] = Command(
6872 ("perm", "list"), Id(help_ref='id:entity_ext'),
6873 fs=FormatSuggestion("%-8s %-8s %-8i",
6874 ("entity_id", "op_set_id", "op_target_id"),
6875 hdr="%-8s %-8s %-8s" %
6876 ("entity_id", "op_set_id", "op_target_id")),
6877 perm_filter='is_superuser')
6878 def perm_list(self, operator, entity_id):
6879 if not self.ba.is_superuser(operator.get_entity_id()):
6880 raise PermissionDenied("Currently limited to superusers")
6881 if entity_id.startswith("group:"):
6882 entities = [ self._get_group(entity_id.split(":")[-1]).entity_id ]
6883 elif entity_id.startswith("account:"):
6884 account = self._get_account(entity_id.split(":")[-1])
6885 group = self.Group_class(self.db)
6886 entities = [account.entity_id]
6887 entities.extend([x["group_id"] for x in
6888 group.search(member_id=account.entity_id,
6889 indirect_members=False)])
6890 else:
6891 if not entity_id.isdigit():
6892 raise CerebrumError("Expected entity-id")
6893 entities = [int(entity_id)]
6894 bar = BofhdAuthRole(self.db)
6895 ret = []
6896 for r in bar.list(entities):
6897 ret.append({'entity_id': self._get_entity_name(r['entity_id']),
6898 'op_set_id': self.num2op_set_name[int(r['op_set_id'])],
6899 'op_target_id': r['op_target_id']})
6900 return ret
6901
6902 # perm grant
6903 all_commands['perm_grant'] = Command(
6904 ("perm", "grant"), Id(), SimpleString(help_ref="string_op_set"),
6905 Id(help_ref="id:op_target"), perm_filter='is_superuser')
6906 def perm_grant(self, operator, entity_id, op_set_name, op_target_id):
6907 if not self.ba.is_superuser(operator.get_entity_id()):
6908 raise PermissionDenied("Currently limited to superusers")
6909 bar = BofhdAuthRole(self.db)
6910 aos = BofhdAuthOpSet(self.db)
6911 aos.find_by_name(op_set_name)
6912
6913 bar.grant_auth(entity_id, aos.op_set_id, op_target_id)
6914 return "OK, granted %s@%s to %s" % (op_set_name, op_target_id,
6915 entity_id)
6916
6917 # perm revoke
6918 all_commands['perm_revoke'] = Command(
6919 ("perm", "revoke"), Id(), SimpleString(help_ref="string_op_set"),
6920 Id(help_ref="id:op_target"), perm_filter='is_superuser')
6921 def perm_revoke(self, operator, entity_id, op_set_name, op_target_id):
6922 if not self.ba.is_superuser(operator.get_entity_id()):
6923 raise PermissionDenied("Currently limited to superusers")
6924 bar = BofhdAuthRole(self.db)
6925 aos = BofhdAuthOpSet(self.db)
6926 aos.find_by_name(op_set_name)
6927 bar.revoke_auth(entity_id, aos.op_set_id, op_target_id)
6928 return "OK, revoked %s@%s from %s" % (op_set_name, op_target_id,
6929 entity_id)
6930
6931 # perm who_has_perm
6932 all_commands['perm_who_has_perm'] = Command(
6933 ("perm", "who_has_perm"), SimpleString(help_ref="string_op_set"),
6934 fs=FormatSuggestion("%-8s %-8s %-8i",
6935 ("entity_id", "op_set_id", "op_target_id"),
6936 hdr="%-8s %-8s %-8s" %
6937 ("entity_id", "op_set_id", "op_target_id")),
6938 perm_filter='is_superuser')
6939 def perm_who_has_perm(self, operator, op_set_name):
6940 if not self.ba.is_superuser(operator.get_entity_id()):
6941 raise PermissionDenied("Currently limited to superusers")
6942 aos = BofhdAuthOpSet(self.db)
6943 aos.find_by_name(op_set_name)
6944 bar = BofhdAuthRole(self.db)
6945 ret = []
6946 for r in bar.list(op_set_id=aos.op_set_id):
6947 ret.append({'entity_id': self._get_entity_name(r['entity_id']),
6948 'op_set_id': self.num2op_set_name[int(r['op_set_id'])],
6949 'op_target_id': r['op_target_id']})
6950 return ret
6951
6952 # perm who_owns
6953 all_commands['perm_who_owns'] = Command(
6954 ("perm", "who_owns"), Id(help_ref="id:entity_ext"),
6955 fs=FormatSuggestion("%-8s %-8s %-8i",
6956 ("entity_id", "op_set_id", "op_target_id"),
6957 hdr="%-8s %-8s %-8s" %
6958 ("entity_id", "op_set_id", "op_target_id")),
6959 perm_filter='is_superuser')
6960 def perm_who_owns(self, operator, id):
6961 if not self.ba.is_superuser(operator.get_entity_id()):
6962 raise PermissionDenied("Currently limited to superusers")
6963 bar = BofhdAuthRole(self.db)
6964 if id.startswith("group:"):
6965 group = self._get_group(id.split(":")[-1])
6966 aot = BofhdAuthOpTarget(self.db)
6967 target_ids = []
6968 for r in aot.list(target_type='group', entity_id=group.entity_id):
6969 target_ids.append(r['op_target_id'])
6970 elif id.startswith("account:"):
6971 account = self._get_account(id.split(":")[-1])
6972 disk = Utils.Factory.get('Disk')(self.db)
6973 try:
6974 tmp = account.get_home(self.const.spread_uit_nis_user)
6975 disk.find(tmp[0])
6976 except Errors.NotFoundError:
6977 raise CerebrumError, "Unknown disk for user"
6978 aot = BofhdAuthOpTarget(self.db)
6979 target_ids = []
6980 for r in aot.list(target_type='global_host'):
6981 target_ids.append(r['op_target_id'])
6982 for r in aot.list(target_type='disk', entity_id=disk.entity_id):
6983 target_ids.append(r['op_target_id'])
6984 for r in aot.list(target_type='host', entity_id=disk.host_id):
6985 if (not r['attr'] or
6986 re.compile(r['attr']).match(disk.path.split("/")[-1]) != None):
6987 target_ids.append(r['op_target_id'])
6988 else:
6989 if not id.isdigit():
6990 raise CerebrumError("Expected target-id")
6991 target_ids = [int(id)]
6992 if not target_ids:
6993 raise CerebrumError("No target_ids for %s" % id)
6994 ret = []
6995 for r in bar.list_owners(target_ids):
6996 ret.append({'entity_id': self._get_entity_name(r['entity_id']),
6997 'op_set_id': self.num2op_set_name[int(r['op_set_id'])],
6998 'op_target_id': r['op_target_id']})
6999 return ret
7000
7001 #
7002 # person commands
7003 #
7004
7005 # person accounts
7006 all_commands['person_accounts'] = Command(
7007 ("person", "accounts"), PersonId(),
7008 fs=FormatSuggestion("%9i %-10s %s",
7009 ("account_id", "name", format_day("expire")),
7010 hdr=("%9s %-10s %s") %
7011 ("Id", "Name", "Expire")))
7012 def person_accounts(self, operator, id):
7013 person = self.util.get_target(id, restrict_to=['Person', 'Group'])
7014 account = self.Account_class(self.db)
7015 ret = []
7016 for r in account.list_accounts_by_owner_id(person.entity_id,
7017 owner_type=person.entity_type,
7018 filter_expired=False):
7019 account = self._get_account(r['account_id'], idtype='id')
7020
7021 ret.append({'account_id': r['account_id'],
7022 'name': account.account_name,
7023 'expire': account.expire_date})
7024 ret.sort(lambda a,b: cmp(a['name'], b['name']))
7025 return ret
7026
7027 def _person_affiliation_add_helper(self, operator, person, ou, aff, aff_status):
7028 """Helper-function for adding an affiliation to a person with
7029 permission checking. person is expected to be a person
7030 object, while ou, aff and aff_status should be the textual
7031 representation from the client"""
7032 aff = self._get_affiliationid(aff)
7033 aff_status = self._get_affiliation_statusid(aff, aff_status)
7034 ou = self._get_ou(stedkode=ou)
7035
7036 # Assert that the person already have the affiliation
7037 has_aff = False
7038 for a in person.get_affiliations():
7039 if a['ou_id'] == ou.entity_id and a['affiliation'] == aff:
7040 if a['status'] == aff_status:
7041 has_aff = True
7042 elif a['source_system'] == self.const.system_manual:
7043 raise CerebrumError, ("Person has conflicting aff_status "
7044 "for this OU/affiliation combination")
7045 if not has_aff:
7046 self.ba.can_add_affiliation(operator.get_entity_id(),
7047 person, ou, aff, aff_status)
7048 # if (aff == self.const.affiliation_ansatt or
7049 # aff == self.const.affiliation_student):
7050 # raise PermissionDenied(
7051 # "Student/Ansatt affiliation can only be set by automatic import routines")
7052 person.add_affiliation(ou.entity_id, aff,
7053 self.const.system_manual, aff_status)
7054 person.write_db()
7055 return ou, aff, aff_status
7056
7057 # person affilation_add
7058 all_commands['person_affiliation_add'] = Command(
7059 ("person", "affiliation_add"), PersonId(help_ref="person_id_other"),
7060 OU(), Affiliation(), AffiliationStatus(),
7061 perm_filter='can_add_affiliation')
7062 def person_affiliation_add(self, operator, person_id, ou, aff, aff_status):
7063 try:
7064 person = self._get_person(*self._map_person_id(person_id))
7065 except Errors.TooManyRowsError:
7066 raise CerebrumError("Unexpectedly found more than one person")
7067 ou, aff, aff_status = self._person_affiliation_add_helper(
7068 operator, person, ou, aff, aff_status)
7069 return "OK, added %s@%s to %s" % (aff, self._format_ou_name(ou), person.entity_id)
7070
7071 # person affilation_remove
7072 all_commands['person_affiliation_remove'] = Command(
7073 ("person", "affiliation_remove"), PersonId(), OU(), Affiliation(),
7074 perm_filter='can_remove_affiliation')
7075 def person_affiliation_remove(self, operator, person_id, ou, aff):
7076 try:
7077 person = self._get_person(*self._map_person_id(person_id))
7078 except Errors.TooManyRowsError:
7079 raise CerebrumError("Unexpectedly found more than one person")
7080 aff = self._get_affiliationid(aff)
7081 ou = self._get_ou(stedkode=ou)
7082 auth_systems = []
7083 for auth_sys in cereconf.BOFHD_AUTH_SYSTEMS:
7084 tmp=getattr(self.const, auth_sys)
7085 auth_systems.append(int(tmp))
7086 self.ba.can_remove_affiliation(operator.get_entity_id(), person, ou, aff)
7087 for row in person.list_affiliations(person_id=person.entity_id,
7088 affiliation=aff):
7089 if row['ou_id'] != int(ou.entity_id):
7090 continue
7091 if not int(row['source_system']) in auth_systems:
7092 person.delete_affiliation(ou.entity_id, aff,
7093 row['source_system'])
7094 else:
7095 raise CerebrumError("Cannot remove affiliation registered from an authoritative source system")
7096 return "OK, removed %s@%s from %s" % (aff, self._format_ou_name(ou), person.entity_id)
7097
7098 # person set_bdate
7099 all_commands['person_set_bdate'] = Command(
7100 ("person", "set_bdate"), PersonId(help_ref="id:target:person"),
7101 Date(help_ref='date_birth'), perm_filter='can_create_person')
7102 def person_set_bdate(self, operator, person_id, bdate):
7103 self.ba.can_create_person(operator.get_entity_id())
7104 try:
7105 person = self.util.get_target(person_id, restrict_to=['Person'])
7106 except Errors.TooManyRowsError:
7107 raise CerebrumError("Unexpectedly found more than one person")
7108 for a in person.get_affiliations():
7109 if (int(a['source_system']) in
7110 [int(self.const.system_fs), int(self.const.system_sap)]):
7111 raise PermissionDenied("You are not allowed to alter birth date for this person.")
7112 bdate = self._parse_date(bdate)
7113 if bdate > self._today():
7114 raise CerebrumError, "Please check the date of birth, cannot register date_of_birth > now"
7115 person.birth_date = bdate
7116 person.write_db()
7117 return "OK, set birth date for '%s' = '%s'" % (person_id, bdate)
7118
7119 # person set_name
7120 all_commands['person_set_name'] = Command(
7121 ("person", "set_name"), PersonId(help_ref="person_id_other"),
7122 PersonName(help_ref="person_name_first"),
7123 PersonName(help_ref="person_name_last"),
7124 fs=FormatSuggestion("Name altered for: %i", ("person_id",)),
7125 perm_filter='can_create_person')
7126
7127 def person_set_name(self, operator, person_id, first_name, last_name):
7128 auth_systems = []
7129 for auth_sys in cereconf.BOFHD_AUTH_SYSTEMS:
7130 tmp = getattr(self.const, auth_sys)
7131 auth_systems.append(int(tmp))
7132 person = self._get_person(*self._map_person_id(person_id))
7133 self.ba.can_create_person(operator.get_entity_id())
7134 for a in person.get_affiliations():
7135 if int(a['source_system']) in auth_systems:
7136 raise PermissionDenied("You are not allowed to alter "
7137 "names registered in authoritative "
7138 "source_systems.")
7139
7140 if last_name == "":
7141 raise CerebrumError("Last name is required.")
7142
7143 if first_name == "":
7144 full_name = last_name
7145 else:
7146 full_name = " ".join((first_name, last_name))
7147
7148 person.affect_names(self.const.system_manual,
7149 self.const.name_first,
7150 self.const.name_last,
7151 self.const.name_full)
7152
7153 # If first_name is an empty string, it should remain unpopulated.
7154 # Since it is tagged as an affected name_variant above, this will
7155 # trigger the original name_variant-row in the db to be deleted when
7156 # running write_db.
7157 if first_name != "":
7158 person.populate_name(self.const.name_first, first_name)
7159
7160 person.populate_name(self.const.name_last, last_name)
7161 person.populate_name(self.const.name_full, full_name)
7162
7163 try:
7164 person.write_db()
7165 except self.db.DatabaseError, m:
7166 raise CerebrumError("Database error: %s" % m)
7167
7168 return {'person_id': person.entity_id}
7169
7170 # person name_suggestions
7171 hidden_commands['person_name_suggestions'] = Command(
7172 ('person', 'name_suggestions'),
7173 PersonId(help_ref='person_id_other'))
7174 def person_name_suggestions(self, operator, person_id):
7175 """Return a list of names that the user can choose for himself. Each
7176 name could generate a different primary e-mail address, so this is also
7177 returned.
7178
7179 The name varieties are generated:
7180
7181 - The primary family name is used as a basis for all varieties.
7182
7183 - All given names are then added in front of the family name. If the
7184 given name contains several names, all of these are added as a
7185 variety, e.g:
7186
7187 family: Doe, given: John Robert
7188 varieties: John Doe, John Robert Doe, Robert Doe
7189 """
7190 person = self._get_person(*self._map_person_id(person_id))
7191 account = self._get_account(operator.get_entity_id(), idtype='id')
7192 if not (self.ba.is_superuser(operator.get_entity_id()) or
7193 account.owner_id == person.entity_id):
7194 raise CerebrumError('You can only get your own names')
7195
7196 # get primary last name to use for basis
7197 last_name = None
7198 for sys in cereconf.SYSTEM_LOOKUP_ORDER:
7199 try:
7200 last_name = person.get_name(getattr(self.const, sys),
7201 self.const.name_last)
7202 if last_name:
7203 break
7204 except Errors.NotFoundError:
7205 pass
7206 if not last_name:
7207 raise CerebrumError('Found no family name for person')
7208
7209 def name_combinations(names):
7210 """Return all different combinations of given names, while keeping
7211 the order intact."""
7212 ret = []
7213 for i in range(len(names)):
7214 ret.append([names[i]])
7215 ret.extend([names[i]] + nxt
7216 for nxt in name_combinations(names[i+1:]))
7217 return ret
7218
7219 names = set()
7220 for sys in cereconf.SYSTEM_LOOKUP_ORDER:
7221 try:
7222 name = person.get_name(getattr(self.const, sys),
7223 self.const.name_first)
7224 except Errors.NotFoundError:
7225 continue
7226 names.update((tuple(n) + (last_name,))
7227 for n in name_combinations(name.split(' ')))
7228 account.clear()
7229
7230 uidaddr = True
7231 # TODO: what if person has no primary account?
7232 try:
7233 account.find(person.get_primary_account())
7234 ed = Email.EmailDomain(self.db)
7235 ed.find(account.get_primary_maildomain())
7236 domain = ed.email_domain_name
7237 for cat in ed.get_categories():
7238 if int(cat['category'] == int(self.const.email_domain_category_cnaddr)):
7239 uidaddr = False
7240 except Errors.NotFoundError:
7241 domain = 'ulrik.uit.no'
7242 if uidaddr:
7243 return [(name, '%s@%s' % (account.account_name, domain))
7244 for name in names]
7245 return [(name,
7246 '%s@%s' % (account.get_email_cn_given_local_part(' '.join(name)),
7247 domain))
7248 for name in names]
7249
7250 # person create
7251 all_commands['person_create'] = Command(
7252 ("person", "create"), PersonId(),
7253 Date(help_ref='date_birth'), PersonName(help_ref='person_name_first'),
7254 PersonName(help_ref='person_name_last'), OU(), Affiliation(),
7255 AffiliationStatus(),
7256 fs=FormatSuggestion("Created: %i",
7257 ("person_id",)), perm_filter='can_create_person')
7258 def person_create(self, operator, person_id, bdate, person_name_first,
7259 person_name_last, ou, affiliation, aff_status):
7260 stedkode = ou
7261 try:
7262 ou = self._get_ou(stedkode=ou)
7263 except Errors.NotFoundError:
7264 raise CerebrumError, "Unknown OU (%s)" % ou
7265 try:
7266 aff = self._get_affiliationid(affiliation)
7267 except Errors.NotFoundError:
7268 raise CerebrumError, "Unknown affiliation type (%s)" % affiliation
7269 self.ba.can_create_person(operator.get_entity_id(), ou, aff)
7270 person = Utils.Factory.get('Person')(self.db)
7271 person.clear()
7272 # TBD: The current implementation of ._parse_date() should
7273 # handle None input just fine; if that implementation is
7274 # correct, this test can be removed.
7275 if bdate is not None:
7276 bdate = self._parse_date(bdate)
7277 if bdate > self._today():
7278 raise CerebrumError, "Please check the date of birth, cannot register date_of_birth > now"
7279 if person_id:
7280 id_type, id = self._map_person_id(person_id)
7281 else:
7282 id_type = None
7283 gender = self.const.gender_unknown
7284 if id_type is not None and id:
7285 if id_type == self.const.externalid_fodselsnr:
7286 try:
7287 if fodselsnr.er_mann(id):
7288 gender = self.const.gender_male
7289 else:
7290 gender = self.const.gender_female
7291 except fodselsnr.InvalidFnrError, msg:
7292 raise CerebrumError("Invalid birth-no: '%s'" % msg)
7293 try:
7294 person.find_by_external_id(self.const.externalid_fodselsnr, id)
7295 raise CerebrumError("A person with that fnr already exists")
7296 except Errors.TooManyRowsError:
7297 raise CerebrumError("A person with that fnr already exists")
7298 except Errors.NotFoundError:
7299 pass
7300 person.clear()
7301 self._person_create_externalid_helper(person)
7302 person.populate_external_id(self.const.system_manual,
7303 self.const.externalid_fodselsnr,
7304 id)
7305 person.populate(bdate, gender,
7306 description='Manually created')
7307 person.affect_names(self.const.system_manual, self.const.name_first, self.const.name_last)
7308 person.populate_name(self.const.name_first,
7309 person_name_first)
7310 person.populate_name(self.const.name_last,
7311 person_name_last)
7312 try:
7313 person.write_db()
7314 self._person_affiliation_add_helper(
7315 operator, person, stedkode, str(aff), aff_status)
7316 except self.db.DatabaseError, m:
7317 raise CerebrumError, "Database error: %s" % m
7318 return {'person_id': person.entity_id}
7319
7320 def _person_create_externalid_helper(self, person):
7321 person.affect_external_id(self.const.system_manual,
7322 self.const.externalid_fodselsnr)
7323 # person find
7324 all_commands['person_find'] = Command(
7325 ("person", "find"), PersonSearchType(), SimpleString(),
7326 SimpleString(optional=True, help_ref="affiliation_optional"),
7327 fs=FormatSuggestion("%7i %10s %-12s %s",
7328 ('id', 'birth', 'account', 'name'),
7329 hdr="%7s %10s %-12s %s" % \
7330 ('Id', 'Birth', 'Account', 'Name')))
7331 def person_find(self, operator, search_type, value, filter=None):
7332 # TODO: Need API support for this
7333 matches = []
7334 idcol = 'person_id'
7335 if filter is not None:
7336 try:
7337 filter = int(self.const.PersonAffiliation(filter))
7338 except Errors.NotFoundError:
7339 raise CerebrumError, ("Invalid affiliation '%s' (perhaps you "
7340 "need to quote the arguments?)" % filter)
7341 person = Utils.Factory.get('Person')(self.db)
7342 person.clear()
7343 extids = {
7344 'fnr': 'externalid_fodselsnr',
7345 'passnr': 'externalid_pass_number',
7346 'ssn': 'externalid_social_security_number',
7347 'taxid': 'externalid_tax_identification_number',
7348 'vatnr': 'externalid_value_added_tax_number',
7349 'studnr': 'externalid_studentnr',
7350 'sapnr': 'externalid_sap_ansattnr'
7351 }
7352 if search_type == 'name':
7353 if filter is not None:
7354 raise CerebrumError("Can't filter by affiliation "
7355 "for search type 'name'")
7356 if len(value.strip(" \t%_*?")) < 3:
7357 raise CerebrumError("You must specify at least three "
7358 "letters of the name")
7359 matches = person.search_person_names(name=value,
7360 name_variant=self.const.name_full,
7361 source_system=self.const.system_cached,
7362 exact_match=False,
7363 case_sensitive=(value != value.lower()))
7364 elif search_type in extids:
7365 idtype = getattr(self.const, extids[search_type], None)
7366 if idtype:
7367 matches = person.list_external_ids(
7368 id_type=idtype,
7369 external_id=value)
7370 idcol = 'entity_id'
7371 else:
7372 raise CerebrumError, "Unknown search type (%s)" % search_type
7373 elif search_type == 'date':
7374 matches = person.find_persons_by_bdate(self._parse_date(value))
7375 elif search_type == 'stedkode':
7376 ou = self._get_ou(stedkode=value)
7377 matches = person.list_affiliations(ou_id=ou.entity_id,
7378 affiliation=filter)
7379 elif search_type == 'ou':
7380 ou = self._get_ou(ou_id=value)
7381 matches = person.list_affiliations(ou_id=ou.entity_id,
7382 affiliation=filter)
7383 else:
7384 raise CerebrumError, "Unknown search type (%s)" % search_type
7385 ret = []
7386 seen = {}
7387 acc = self.Account_class(self.db)
7388 # matches may be an iterator, so force it into a list so we
7389 # can count the entries.
7390 matches = list(matches)
7391 if len(matches) > cereconf.BOFHD_MAX_MATCHES:
7392 raise CerebrumError, ("More than %d (%d) matches, please narrow "
7393 "search criteria" % (cereconf.BOFHD_MAX_MATCHES,
7394 len(matches)))
7395 for row in matches:
7396 # We potentially get multiple rows for a person when
7397 # s/he has more than one source system or affiliation.
7398 p_id = row[idcol]
7399 if p_id in seen:
7400 continue
7401 seen[p_id] = True
7402 person.clear()
7403 person.find(p_id)
7404 if row.has_key('name'):
7405 pname = row['name']
7406 else:
7407 try:
7408 pname = person.get_name(self.const.system_cached,
7409 getattr(self.const,
7410 cereconf.DEFAULT_GECOS_NAME))
7411 except Errors.NotFoundError:
7412 # Oh well, we don't know person's name
7413 pname = '<none>'
7414
7415 # Person.get_primary_account will not return expired
7416 # users. Account.get_account_types will return the
7417 # accounts ordered by priority, but the highest priority
7418 # might be expired.
7419 account_name = "<none>"
7420 for row in acc.get_account_types(owner_id=p_id,
7421 filter_expired=False):
7422 acc.clear()
7423 acc.find(row['account_id'])
7424 account_name = acc.account_name
7425 if not acc.is_expired():
7426 break
7427
7428 # Ideally we'd fetch the authoritative last name, but
7429 # it's a lot of work. We cheat and use the last word
7430 # of the name, which should work for 99.9% of the users.
7431 ret.append({'id': p_id,
7432 'birth': date_to_string(person.birth_date),
7433 'export_id': person.export_id,
7434 'account': account_name,
7435 'name': pname,
7436 'lastname': pname.split(" ")[-1] })
7437 ret.sort(lambda a,b: (cmp(a['lastname'], b['lastname']) or
7438 cmp(a['name'], b['name'])))
7439 return ret
7440
7441 # person info
7442 all_commands['person_info'] = Command(
7443 ("person", "info"), PersonId(help_ref="id:target:person"),
7444 fs=FormatSuggestion([
7445 ("Name: %s\n" +
7446 "Entity-id: %i\n" +
7447 "Export ID: %s\n" +
7448 "Birth: %s\n" +
7449 "Deceased: %s\n" +
7450 "Spreads: %s\n" +
7451 "Affiliations: %s [from %s]",
7452 ("name", "entity_id", "export_id", "birth", "deceased", "spreads",
7453 "affiliation_1", "source_system_1")),
7454 (" %s [from %s]",
7455 ("affiliation", "source_system")),
7456 ("Names: %s[from %s]",
7457 ("names", "name_src")),
7458 ("Fnr: %s [from %s]",
7459 ("fnr", "fnr_src")),
7460 ("Contact: %s: %s [from %s]",
7461 ("contact_type", "contact", "contact_src")),
7462 ("External id: %s [from %s]",
7463 ("extid", "extid_src"))
7464 ]))
7465 def person_info(self, operator, person_id):
7466 try:
7467 person = self.util.get_target(person_id, restrict_to=['Person'])
7468 except Errors.TooManyRowsError:
7469 raise CerebrumError("Unexpectedly found more than one person")
7470 try:
7471 p_name = person.get_name(self.const.system_cached,
7472 getattr(self.const, cereconf.DEFAULT_GECOS_NAME))
7473 p_name = p_name + ' [from Cached]'
7474 except Errors.NotFoundError:
7475 raise CerebrumError("No name is registered for this person")
7476 data = [{'name': p_name,
7477 'entity_id': person.entity_id,
7478 'export_id': person.export_id,
7479 'birth': date_to_string(person.birth_date),
7480 'deceased': date_to_string(person.deceased_date),
7481 'spreads': ", ".join([str(self.const.Spread(x['spread']))
7482 for x in person.get_spread()])}]
7483 affiliations = []
7484 sources = []
7485 last_dates = []
7486 for row in person.get_affiliations():
7487 ou = self._get_ou(ou_id=row['ou_id'])
7488 date = row['last_date'].strftime("%Y-%m-%d")
7489 last_dates.append(date)
7490 affiliations.append("%s@%s" % (
7491 self.const.PersonAffStatus(row['status']),
7492 self._format_ou_name(ou)))
7493 sources.append(str(self.const.AuthoritativeSystem(row['source_system'])))
7494 for ss in cereconf.SYSTEM_LOOKUP_ORDER:
7495 ss = getattr(self.const, ss)
7496 person_name = ""
7497 for type in [self.const.name_first, self.const.name_last]:
7498 try:
7499 person_name += person.get_name(ss, type) + ' '
7500 except Errors.NotFoundError:
7501 continue
7502 if person_name:
7503 data.append({'names': person_name,
7504 'name_src': str(
7505 self.const.AuthoritativeSystem(ss))})
7506 if affiliations:
7507 data[0]['affiliation_1'] = affiliations[0]
7508 data[0]['source_system_1'] = sources[0]
7509 data[0]['last_date_1'] = last_dates[0]
7510 else:
7511 data[0]['affiliation_1'] = "<none>"
7512 data[0]['source_system_1'] = "<nowhere>"
7513 data[0]['last_date_1'] = "<none>"
7514 for i in range(1, len(affiliations)):
7515 data.append({'affiliation': affiliations[i],
7516 'source_system': sources[i],
7517 'last_date': last_dates[i]})
7518 account = self.Account_class(self.db)
7519 account_ids = [int(r['account_id'])
7520 for r in account.list_accounts_by_owner_id(person.entity_id)]
7521 ## Ugly hack: We use membership in a given group (defined in
7522 ## cereconf) to enable viewing fnr in person info.
7523 is_member_of_priviliged_group = False
7524 if cereconf.BOFHD_FNR_ACCESS_GROUP is not None:
7525 g_view_fnr = Utils.Factory.get("Group")(self.db)
7526 g_view_fnr.find_by_name(cereconf.BOFHD_FNR_ACCESS_GROUP)
7527 is_member_of_priviliged_group = g_view_fnr.has_member(operator.get_entity_id())
7528 if (self.ba.is_superuser(operator.get_entity_id()) or
7529 operator.get_entity_id() in account_ids or
7530 is_member_of_priviliged_group):
7531 # Show fnr
7532 for row in person.get_external_id(id_type=self.const.externalid_fodselsnr):
7533 data.append({'fnr': row['external_id'],
7534 'fnr_src': str(
7535 self.const.AuthoritativeSystem(row['source_system']))})
7536
7537 for row in person.get_external_id(id_type=self.const.externalid_studentnr):
7538 data.append({'studentnr' : row['external_id'],
7539 'studentnr_src' : str(
7540 self.const.AuthoritativeSystem(row['source_system']))})
7541 for row in person.get_external_id(id_type=self.const.externalid_hifm_ansattnr):
7542 data.append({'Hif_ansattnr': row['external_id'],
7543 'ansattnr_src': str(
7544 self.const.AuthoritativeSystem(row['source_system']))})
7545 for row in person.get_external_id(id_type=self.const.externalid_paga_ansattnr):
7546 data.append({'uit_ansattnr': row['external_id'],
7547 'ansattnr_src': str(
7548 self.const.AuthoritativeSystem(row['source_system']))})
7549
7550 # Show external id from FS and PAGA
7551 for extid in ('externalid_paga_ansattnr',
7552 'externalid_studentnr',
7553 'externalid_pass_number',
7554 'externalid_social_security_number',
7555 'externalid_tax_identification_number',
7556 'externalid_value_added_tax_number'):
7557 extid = getattr(self.const, extid, None)
7558 if extid:
7559 for row in person.get_external_id(id_type=extid):
7560 data.append({'extid': row['external_id'],
7561 'extid_src': str(
7562 self.const.AuthoritativeSystem(row['source_system']))})
7563 # Show contact info
7564 for row in person.get_contact_info():
7565 if row['contact_type'] not in (self.const.contact_phone,
7566 self.const.contact_mobile_phone,
7567 self.const.contact_phone_private,
7568 self.const.contact_private_mobile):
7569 continue
7570 try:
7571 if self.ba.can_get_contact_info(
7572 operator.get_entity_id(),
7573 person=person,
7574 contact_type=str(self.const.ContactInfo(
7575 row['contact_type']))):
7576 data.append({
7577 'contact': row['contact_value'],
7578 'contact_src': str(self.const.AuthoritativeSystem(
7579 row['source_system'])),
7580 'contact_type': str(self.const.ContactInfo(
7581 row['contact_type']))
7582 })
7583 except PermissionDenied:
7584 continue
7585 return data
7586
7587 # person set_id
7588 all_commands['person_set_id'] = Command(
7589 ("person", "set_id"), PersonId(help_ref="person_id:current"),
7590 PersonId(help_ref="person_id:new"), SourceSystem(help_ref="source_system"))
7591 def person_set_id(self, operator, current_id, new_id, source_system):
7592 if not self.ba.is_superuser(operator.get_entity_id()):
7593 raise PermissionDenied("Currently limited to superusers")
7594 person = self._get_person(*self._map_person_id(current_id))
7595 idtype, id = self._map_person_id(new_id)
7596 self.ba.can_set_person_id(operator.get_entity_id(), person, idtype)
7597 if not source_system:
7598 ss = self.const.system_manual
7599 else:
7600 ss = int(self.const.AuthoritativeSystem(source_system))
7601 person.affect_external_id(ss, idtype)
7602 person.populate_external_id(ss, idtype, id)
7603 person.write_db()
7604 return "OK, set '%s' as new id for '%s'" % (new_id, current_id)
7605
7606 # person clear_id
7607 all_commands['person_clear_id'] = Command(
7608 ("person", "clear_id"), PersonId(),
7609 SourceSystem(help_ref="source_system"), ExternalIdType(),
7610 perm_filter='is_superuser')
7611 def person_clear_id(self, operator, person_id, source_system, idtype):
7612 if not self.ba.is_superuser(operator.get_entity_id()):
7613 raise PermissionDenied("Currently limited to superusers")
7614 person = self.util.get_target(person_id, restrict_to="Person")
7615 ss = self.const.AuthoritativeSystem(source_system)
7616 try:
7617 int(ss)
7618 except Errors.NotFoundError:
7619 raise CerebrumError("No such source system")
7620
7621 idtype = self.const.EntityExternalId(idtype)
7622 try:
7623 int(idtype)
7624 except Errors.NotFoundError:
7625 raise CerebrumError("No such external id")
7626
7627 try:
7628 person._delete_external_id(ss, idtype)
7629 except:
7630 raise CerebrumError("Could not delete id %s:%s for %s" %
7631 (idtype, source_system, person_id))
7632 return "OK"
7633 # end person_clear_id
7634
7635
7636 # person clear_name
7637 all_commands['person_clear_name'] = Command(
7638 ("person", "clear_name"),PersonId(help_ref="person_id_other"),
7639 SourceSystem(help_ref="source_system"),
7640 perm_filter='can_clear_name')
7641 def person_clear_name(self, operator, person_id, source_system):
7642 person = self.util.get_target(person_id, restrict_to="Person")
7643 ss = self.const.AuthoritativeSystem(source_system)
7644 try:
7645 int(ss)
7646 except Errors.NotFoundError:
7647 raise CerebrumError("No such source system")
7648 self.ba.can_clear_name(operator.get_entity_id(), person=person,
7649 source_system=ss)
7650 removed = False
7651 for variant in (self.const.name_first, self.const.name_last, self.const.name_full):
7652 try:
7653 person.get_name(ss, variant)
7654 except Errors.NotFoundError:
7655 continue
7656 try:
7657 person._delete_name(ss, variant)
7658 except:
7659 raise CerebrumError("Could not delete %s from %s" %
7660 (str(variant).lower(), source_system))
7661 removed = True
7662 person._update_cached_names()
7663 if not removed:
7664 return ("No name to remove for %s from %s" %
7665 (person_id, source_system))
7666 return "Removed name for %s from %s" % (person_id, source_system)
7667
7668 # person student_info
7669 all_commands['person_student_info'] = Command(
7670 ("person", "student_info"), PersonId(),
7671 fs=FormatSuggestion([
7672 ("Studieprogrammer: %s, %s, %s, %s, tildelt=%s->%s privatist: %s",
7673 ("studprogkode", "studieretningkode", "studierettstatkode", "studentstatkode",
7674 format_day("dato_tildelt"), format_day("dato_gyldig_til"), "privatist")),
7675 ("Eksamensmeldinger: %s (%s), %s",
7676 ("ekskode", "programmer", format_day("dato"))),
7677 ("Underv.meld: %s, %s",
7678 ("undvkode", format_day("dato"))),
7679 ("Utd. plan: %s, %s, %d, %s",
7680 ("studieprogramkode", "terminkode_bekreft", "arstall_bekreft",
7681 format_day("dato_bekreftet"))),
7682 ("Semesterregistrert: %s - %s, registrert: %s, endret: %s",
7683 ("regstatus", "regformkode", format_day("dato_endring"),
7684 format_day("dato_regform_endret"))),
7685 ("Semesterbetaling: %s - %s, betalt: %s",
7686 ("betstatus", "betformkode", format_day('dato_betaling'))),
7687 ("Registrert med status_dod: %s",
7688 ("status_dod",)),
7689 ]),
7690 perm_filter='can_get_student_info')
7691 def person_student_info(self, operator, person_id):
7692 person_exists = False
7693 person = None
7694 try:
7695 person = self._get_person(*self._map_person_id(person_id))
7696 person_exists = True
7697 except CerebrumError, e:
7698 # Check if person exists in FS, but is not imported yet, e.g.
7699 # emnestudents. These should only be listed with limited
7700 # information.
7701 if person_id and len(person_id) == 11 and person_id.isdigit():
7702 try:
7703 person_id = fodselsnr.personnr_ok(person_id)
7704 except:
7705 raise e
7706 self.logger.debug('Unknown person %s, asking FS directly', person_id)
7707 self.ba.can_get_student_info(operator.get_entity_id(), None)
7708 fodselsdato, pnum = person_id[:6], person_id[6:]
7709 else:
7710 raise e
7711 else:
7712 self.ba.can_get_student_info(operator.get_entity_id(), person)
7713 fnr = person.get_external_id(id_type=self.const.externalid_fodselsnr,
7714 source_system=self.const.system_fs)
7715 if not fnr:
7716 raise CerebrumError("No matching fnr from FS")
7717 fodselsdato, pnum = fodselsnr.del_fnr(fnr[0]['external_id'])
7718 har_opptak = {}
7719 ret = []
7720 try:
7721 db = Database.connect(user=cereconf.FS_USER,
7722 service=cereconf.FS_DATABASE_NAME,
7723 DB_driver=cereconf.DB_DRIVER_ORACLE)
7724 except Database.DatabaseError, e:
7725 self.logger.warn("Can't connect to FS (%s)" % e)
7726 raise CerebrumError("Can't connect to FS, try later")
7727 fs = FS(db)
7728 for row in fs.student.get_undervisningsmelding(fodselsdato, pnum):
7729 ret.append({'undvkode': row['emnekode'],
7730 'dato': row['dato_endring'],})
7731
7732 if person_exists:
7733 for row in fs.student.get_studierett(fodselsdato, pnum):
7734 har_opptak["%s" % row['studieprogramkode']] = \
7735 row['status_privatist']
7736 ret.append({'studprogkode': row['studieprogramkode'],
7737 'studierettstatkode': row['studierettstatkode'],
7738 'studentstatkode': row['studentstatkode'],
7739 'studieretningkode': row['studieretningkode'],
7740 'dato_tildelt': row['dato_studierett_tildelt'],
7741 'dato_gyldig_til': row['dato_studierett_gyldig_til'],
7742 'privatist': row['status_privatist']})
7743
7744 for row in fs.student.get_eksamensmeldinger(fodselsdato, pnum):
7745 programmer = []
7746 for row2 in fs.info.get_emne_i_studieprogram(row['emnekode']):
7747 if har_opptak.has_key("%s" % row2['studieprogramkode']):
7748 programmer.append(row2['studieprogramkode'])
7749 ret.append({'ekskode': row['emnekode'],
7750 'programmer': ",".join(programmer),
7751 'dato': row['dato_opprettet']})
7752
7753 for row in fs.student.get_utdanningsplan(fodselsdato, pnum):
7754 ret.append({'studieprogramkode': row['studieprogramkode'],
7755 'terminkode_bekreft': row['terminkode_bekreft'],
7756 'arstall_bekreft': row['arstall_bekreft'],
7757 'dato_bekreftet': row['dato_bekreftet']})
7758
7759 def _ok_or_not(input):
7760 """Helper function for proper feedback of status."""
7761 if not input or input == 'N':
7762 return 'Nei'
7763 if input == 'J':
7764 return 'Ja'
7765 return input
7766
7767 semregs = tuple(fs.student.get_semreg(fodselsdato, pnum,
7768 only_valid=False))
7769 for row in semregs:
7770 ret.append({'regstatus': _ok_or_not(row['status_reg_ok']),
7771 'regformkode': row['regformkode'],
7772 'dato_endring': row['dato_endring'],
7773 'dato_regform_endret': row['dato_regform_endret']})
7774 ret.append({'betstatus': _ok_or_not(row['status_bet_ok']),
7775 'betformkode': row['betformkode'],
7776 'dato_betaling': row['dato_betaling']})
7777 # The semreg and sembet lines should always be sent, to make it
7778 # easier for the IT staff to see if a student have paid or not.
7779 if not semregs:
7780 ret.append({'regstatus': 'Nei',
7781 'regformkode': None,
7782 'dato_endring': None,
7783 'dato_regform_endret': None})
7784 ret.append({'betstatus': 'Nei',
7785 'betformkode': None,
7786 'dato_betaling': None})
7787
7788 # Check is alive
7789 #if fs.person.is_dead(fodselsdato, pnum):
7790 # ret.append({'status_dod': 'Ja'})
7791 db.close()
7792 return ret
7793
7794 # person user_priority
7795 all_commands['person_set_user_priority'] = Command(
7796 ("person", "set_user_priority"), AccountName(),
7797 SimpleString(help_ref='string_old_priority'),
7798 SimpleString(help_ref='string_new_priority'))
7799 def person_set_user_priority(self, operator, account_name,
7800 old_priority, new_priority):
7801 account = self._get_account(account_name)
7802 person = self._get_person('entity_id', account.owner_id)
7803 self.ba.can_set_person_user_priority(operator.get_entity_id(), account)
7804 try:
7805 old_priority = int(old_priority)
7806 new_priority = int(new_priority)
7807 except ValueError:
7808 raise CerebrumError, "priority must be a number"
7809 ou = None
7810 affiliation = None
7811 for row in account.get_account_types(filter_expired=False):
7812 if row['priority'] == old_priority:
7813 ou = row['ou_id']
7814 affiliation = row['affiliation']
7815 if ou is None:
7816 raise CerebrumError("Must specify an existing priority")
7817 account.set_account_type(ou, affiliation, new_priority)
7818 account.write_db()
7819 return "OK, set priority=%i for %s" % (new_priority, account_name)
7820
7821 all_commands['person_list_user_priorities'] = Command(
7822 ("person", "list_user_priorities"), PersonId(),
7823 fs=FormatSuggestion(
7824 "%8s %8i %30s %15s", ('uname', 'priority', 'affiliation', 'status'),
7825 hdr="%8s %8s %30s %15s" % ("Uname", "Priority", "Affiliation", "Status")))
7826 def person_list_user_priorities(self, operator, person_id):
7827 ac = Utils.Factory.get('Account')(self.db)
7828 person = self._get_person(*self._map_person_id(person_id))
7829 ret = []
7830 for row in ac.get_account_types(all_persons_types=True,
7831 owner_id=person.entity_id,
7832 filter_expired=False):
7833 ac2 = self._get_account(row['account_id'], idtype='id')
7834 if ac2.is_expired() or ac2.is_deleted():
7835 status = "Expired"
7836 else:
7837 status = "Active"
7838 ou = self._get_ou(ou_id=row['ou_id'])
7839 ret.append({'uname': ac2.account_name,
7840 'priority': row['priority'],
7841 'affiliation':
7842 '%s@%s' % (self.const.PersonAffiliation(row['affiliation']),
7843 self._format_ou_name(ou)),
7844 'status': status})
7845 return ret
7846
7847 #
7848 # quarantine commands
7849 #
7850
7851 # quarantine disable
7852 all_commands['quarantine_disable'] = Command(
7853 ("quarantine", "disable"), EntityType(default="account"), Id(),
7854 QuarantineType(), Date(), perm_filter='can_disable_quarantine')
7855 def quarantine_disable(self, operator, entity_type, id, qtype, date):
7856 entity = self._get_entity(entity_type, id)
7857 date = self._parse_date(date)
7858 qconst = self._get_constant(self.const.Quarantine, qtype, "quarantine")
7859 self.ba.can_disable_quarantine(operator.get_entity_id(), entity, qtype)
7860
7861 if not entity.get_entity_quarantine(qtype=qconst):
7862 raise CerebrumError("%s does not have a quarantine of type %s" % (
7863 self._get_name_from_object(entity), qtype))
7864
7865 limit = getattr(cereconf, 'BOFHD_QUARANTINE_DISABLE_LIMIT', None)
7866 if limit:
7867 if date > DateTime.today() + DateTime.RelativeDateTime(days=limit):
7868 return "Quarantines can only be disabled for %d days" % limit
7869 if date and date < DateTime.today():
7870 raise CerebrumError("Date can't be in the past")
7871 entity.disable_entity_quarantine(qconst, date)
7872 if not date:
7873 return "OK, reactivated quarantine %s for %s" % (
7874 qconst, self._get_name_from_object(entity))
7875 return "OK, disabled quarantine %s for %s" % (
7876 qconst, self._get_name_from_object(entity))
7877
7878 # quarantine list
7879 all_commands['quarantine_list'] = Command(
7880 ("quarantine", "list"),
7881 fs=FormatSuggestion("%-16s %1s %-17s %s",
7882 ('name', 'lock', 'shell', 'desc'),
7883 hdr="%-15s %-4s %-17s %s" % \
7884 ('Name', 'Lock', 'Shell', 'Description')))
7885 def quarantine_list(self, operator):
7886 ret = []
7887 for c in self.const.fetch_constants(self.const.Quarantine):
7888 lock = 'N'; shell = '-'
7889 rule = cereconf.QUARANTINE_RULES.get(str(c), {})
7890 if 'lock' in rule:
7891 lock = 'Y'
7892 if 'shell' in rule:
7893 shell = rule['shell'].split("/")[-1]
7894 ret.append({'name': "%s" % c,
7895 'lock': lock,
7896 'shell': shell,
7897 'desc': c.description})
7898 return ret
7899
7900 # quarantine remove
7901 all_commands['quarantine_remove'] = Command(
7902 ("quarantine", "remove"), EntityType(default="account"), Id(),
7903 QuarantineType(),
7904 perm_filter='can_remove_quarantine')
7905 def quarantine_remove(self, operator, entity_type, id, qtype):
7906 entity = self._get_entity(entity_type, id)
7907 qconst = self._get_constant(self.const.Quarantine, qtype, "quarantine")
7908 self.ba.can_remove_quarantine(operator.get_entity_id(), entity, qconst)
7909
7910 if not entity.get_entity_quarantine(qtype=qconst):
7911 raise CerebrumError("%s does not have a quarantine of type %s" % (
7912 self._get_name_from_object(entity), qtype))
7913
7914 entity.delete_entity_quarantine(qconst)
7915
7916 return "OK, removed quarantine %s for %s" % (
7917 qconst, self._get_name_from_object (entity))
7918
7919 # quarantine set
7920 all_commands['quarantine_set'] = Command(
7921 ("quarantine", "set"), EntityType(default="account"), Id(repeat=True),
7922 QuarantineType(), SimpleString(help_ref="string_why"),
7923 SimpleString(help_ref="quarantine_start_date", default="today",
7924 optional=True),
7925 perm_filter='can_set_quarantine')
7926 def quarantine_set(self, operator, entity_type, id, qtype, why,
7927 start_date=None):
7928 if not start_date or start_date == 'today':
7929 start_date = self._today()
7930 else:
7931 start_date = self._parse_date(start_date)
7932 entity = self._get_entity(entity_type, id)
7933 qconst = self._get_constant(self.const.Quarantine, qtype, "quarantine")
7934 self.ba.can_set_quarantine(operator.get_entity_id(), entity, qconst)
7935 rows = entity.get_entity_quarantine(qtype=qconst)
7936 if rows:
7937 raise CerebrumError("%s already has a quarantine of type %s" % (
7938 self._get_name_from_object(entity), qtype))
7939 try:
7940 entity.add_entity_quarantine(qconst, operator.get_entity_id(), why,
7941 start_date)
7942 except AttributeError:
7943 raise CerebrumError("Quarantines cannot be set on %s" % entity_type)
7944 return "OK, set quarantine %s for %s" % (
7945 qconst, self._get_name_from_object(entity))
7946
7947 # quarantine show
7948 all_commands['quarantine_show'] = Command(
7949 ("quarantine", "show"), EntityType(default="account"), Id(),
7950 fs=FormatSuggestion("%-14s %-16s %-16s %-14s %-8s %s",
7951 ('type', format_time('start'), format_time('end'),
7952 format_day('disable_until'), 'who', 'why'),
7953 hdr="%-14s %-16s %-16s %-14s %-8s %s" % \
7954 ('Type', 'Start', 'End', 'Disable until', 'Who',
7955 'Why')),
7956 perm_filter='can_show_quarantines')
7957 def quarantine_show(self, operator, entity_type, id):
7958 ret = []
7959 entity = self._get_entity(entity_type, id)
7960 self.ba.can_show_quarantines(operator.get_entity_id(), entity)
7961 for r in entity.get_entity_quarantine():
7962 acc = self._get_account(r['creator_id'], idtype='id')
7963 ret.append({'type': str(self.const.Quarantine(r['quarantine_type'])),
7964 'start': r['start_date'],
7965 'end': r['end_date'],
7966 'disable_until': r['disable_until'],
7967 'who': acc.account_name,
7968 'why': r['description']})
7969 return ret
7970 #
7971 # spread commands
7972 #
7973
7974 # spread add
7975 all_commands['spread_add'] = Command(
7976 ("spread", "add"), EntityType(default='account'), Id(), Spread(),
7977 perm_filter='can_add_spread')
7978 def spread_add(self, operator, entity_type, id, spread):
7979 entity = self._get_entity(entity_type, id)
7980 spread = self._get_constant(self.const.Spread, spread, "spread")
7981 self.ba.can_add_spread(operator.get_entity_id(), entity, spread)
7982
7983 if entity.entity_type != spread.entity_type:
7984 raise CerebrumError(
7985 "Spread '%s' is restricted to '%s', selected entity is '%s'" %
7986 (spread, self.const.EntityType(spread.entity_type),
7987 self.const.EntityType(entity.entity_type)))
7988 # exchange-relatert-jazz
7989 # NB! no checks are implemented in the group-mixin
7990 # as we want to let other clients handle these spreads
7991 # in different manner if needed
7992 # dissallow spread-setting for distribution groups
7993 if cereconf.EXCHANGE_GROUP_SPREAD and \
7994 str(spread) == cereconf.EXCHANGE_GROUP_SPREAD:
7995 return "Please create distribution group via 'group exchange_create' in bofh"
7996 if entity.has_spread(spread):
7997 raise CerebrumError("entity id=%s already has spread=%s" %
7998 (id, spread))
7999 try:
8000 entity.add_spread(spread)
8001 except (Errors.RequiresPosixError, self.db.IntegrityError) as e:
8002 raise CerebrumError(str(e))
8003 entity.write_db()
8004 if entity_type == 'account' and cereconf.POSIX_SPREAD_CODES:
8005 self._spread_sync_group(entity)
8006 if hasattr(self.const, 'spread_uit_nis_fg'):
8007 if entity_type == 'group' and spread == self.const.spread_uit_nis_fg:
8008 ad_spread = self.const.spread_uit_ad_group
8009 if not entity.has_spread(ad_spread):
8010 entity.add_spread(ad_spread)
8011 entity.write_db()
8012 return "OK, added spread %s for %s" % (
8013 spread, self._get_name_from_object(entity))
8014
8015 # spread list
8016 all_commands['spread_list'] = Command(
8017 ("spread", "list"),
8018 fs=FormatSuggestion("%-14s %s", ('name', 'desc'),
8019 hdr="%-14s %s" % ('Name', 'Description')))
8020 def spread_list(self, operator):
8021 """
8022 List out all available spreads.
8023 """
8024 ret = []
8025 spr = Entity.EntitySpread(self.db)
8026 autospreads = [self.const.human2constant(x, self.const.Spread)
8027 for x in getattr(cereconf, 'GROUP_REQUESTS_AUTOSPREADS', ())]
8028 for s in spr.list_spreads():
8029 ret.append({'name': s['spread'],
8030 'desc': s['description'],
8031 'type': s['entity_type_str'],
8032 'type_id': s['entity_type'],
8033 'spread_code': s['spread_code'],
8034 'auto': int(s['spread_code'] in autospreads)})
8035 # int() since boolean doesn't work for brukerinfo
8036 return ret
8037
8038 # spread remove
8039 all_commands['spread_remove'] = Command(
8040 ("spread", "remove"), EntityType(default='account'), Id(), Spread(),
8041 perm_filter='can_add_spread')
8042 def spread_remove(self, operator, entity_type, id, spread):
8043 entity = self._get_entity(entity_type, id)
8044 spread = self._get_constant(self.const.Spread, spread, "spread")
8045 self.ba.can_add_spread(operator.get_entity_id(), entity, spread)
8046 # exchange-relatert-jazz
8047 # make sure that if anyone uses spread remove instead of
8048 # group exchange_remove the appropriate clean-up is still
8049 # done
8050 if (entity_type == 'group' and
8051 entity.has_spread(cereconf.EXCHANGE_GROUP_SPREAD)):
8052 raise CerebrumError(
8053 "Cannot remove spread from distribution groups")
8054 if entity.has_spread(spread):
8055 entity.delete_spread(spread)
8056 else:
8057 txt = "Entity '%s' does not have spread '%s'" % (id, str(spread))
8058 raise CerebrumError, txt
8059 if entity_type == 'account' and cereconf.POSIX_SPREAD_CODES:
8060 self._spread_sync_group(entity)
8061 return "OK, removed spread %s from %s" % (
8062 spread, self._get_name_from_object(entity))
8063
8064 def _spread_sync_group(self, account, group=None):
8065 """Make sure the group has the NIS spreads corresponding to
8066 the NIS spreads of the account. The account and group
8067 arguments may be passed as Entity objects. If group is None,
8068 the group with the same name as account is modified, if it
8069 exists."""
8070
8071 if account.np_type or account.owner_type == self.const.entity_group:
8072 return
8073
8074 if group is None:
8075 name = account.get_name(self.const.account_namespace)
8076 try:
8077 group = self._get_group(name)
8078 except CerebrumError:
8079 return
8080
8081 # FIXME: Identifying personal groups is not a very precise
8082 # process. One alternative would be to use the description:
8083 #
8084 # if not group.description.startswith('Personal file group for '):
8085 # return
8086 #
8087 # The alternative is to use the bofhd_auth tables to see if
8088 # the account has the 'Group-owner' op_set for this group, and
8089 # this is implemented below.
8090
8091 op_set = BofhdAuthOpSet(self.db)
8092 op_set.find_by_name('Group-owner')
8093
8094 baot = BofhdAuthOpTarget(self.db)
8095 targets = baot.list(entity_id=group.entity_id)
8096 if len(targets) == 0:
8097 return
8098 bar = BofhdAuthRole(self.db)
8099 is_moderator = False
8100 for auth in bar.list(op_target_id=targets[0]['op_target_id']):
8101 if (auth['entity_id'] == account.entity_id and
8102 auth['op_set_id'] == op_set.op_set_id):
8103 is_moderator = True
8104 if not is_moderator:
8105 return
8106
8107 mapping = { int(self.const.spread_uit_nis_user):
8108 int(self.const.spread_uit_nis_fg),
8109 int(self.const.spread_uit_ad_account):
8110 int(self.const.spread_uit_ad_group),
8111 int(self.const.spread_ifi_nis_user):
8112 int(self.const.spread_ifi_nis_fg) }
8113 wanted = []
8114 for r in account.get_spread():
8115 spread = int(r['spread'])
8116 if spread in mapping:
8117 wanted.append(mapping[spread])
8118 for r in group.get_spread():
8119 spread = int(r['spread'])
8120 if not spread in mapping.values():
8121 pass
8122 elif spread in wanted:
8123 wanted.remove(spread)
8124 else:
8125 group.delete_spread(spread)
8126 for spread in wanted:
8127 group.add_spread(spread)
8128
8129 #
8130 # trait commands
8131 #
8132
8133 # trait info -- show trait values for an entity
8134 all_commands['trait_info'] = Command(
8135 ("trait", "info"), Id(help_ref="id:target:account"),
8136 # Since the FormatSuggestion sorts by the type and not the order of the
8137 # return data, we send both a string to make it pretty in jbofh, and a
8138 # list to be used by brukerinfo, which is ignored by jbofh.
8139 fs=FormatSuggestion("%s", ('text',)),
8140 perm_filter="can_view_trait")
8141 def trait_info(self, operator, ety_id):
8142 ety = self.util.get_target(ety_id, restrict_to=[])
8143 self.ba.can_view_trait(operator.get_entity_id(), ety=ety)
8144
8145 ety_name = self._get_name_from_object(ety)
8146
8147 text = []
8148 ret = []
8149 for trait, values in ety.get_traits().items():
8150 try:
8151 self.ba.can_view_trait(operator.get_entity_id(), trait=trait,
8152 ety=ety, target=values['target_id'])
8153 except PermissionDenied:
8154 continue
8155
8156 text.append(" Trait: %s" % str(trait))
8157 if values['numval'] is not None:
8158 text.append(" Numeric: %d" % values['numval'])
8159 if values['strval'] is not None:
8160 text.append(" String: %s" % values['strval'])
8161 if values['target_id'] is not None:
8162 target = self.util.get_target(int(values['target_id']))
8163 text.append(" Target: %s (%s)" % (
8164 self._get_entity_name(target.entity_id, target.entity_type),
8165 str(self.const.EntityType(target.entity_type))))
8166 if values['date'] is not None:
8167 text.append(" Date: %s" % values['date'])
8168 values['trait_name'] = str(trait)
8169 ret.append(values)
8170 if text:
8171 text = ["Entity: %s (%s)" % (
8172 ety_name,
8173 str(self.const.EntityType(ety.entity_type)))] + text
8174 return {'text': "\n".join(text), 'traits': ret}
8175 return "%s has no traits" % ety_name
8176
8177 # trait list -- list all entities with trait
8178 all_commands['trait_list'] = Command(
8179 ("trait", "list"), SimpleString(help_ref="trait"),
8180 fs=FormatSuggestion("%-16s %-16s %s", ('trait', 'type', 'name'),
8181 hdr="%-16s %-16s %s" % ('Trait', 'Type', 'Name')),
8182 perm_filter="can_list_trait")
8183 def trait_list(self, operator, trait_name):
8184 trait = self._get_constant(self.const.EntityTrait, trait_name, "trait")
8185 self.ba.can_list_trait(operator.get_entity_id(), trait=trait)
8186 ety = self.Account_class(self.db) # exact class doesn't matter
8187 ret = []
8188 ety_type = str(self.const.EntityType(trait.entity_type))
8189 for row in ety.list_traits(trait, return_name=True):
8190 # TODO: Host, Disk and Person don't use entity_name, so name will
8191 # be <not set>
8192 ret.append({'trait': str(trait),
8193 'type': ety_type,
8194 'name': row['name']})
8195 ret.sort(lambda x,y: cmp(x['name'], y['name']))
8196 return ret
8197
8198 # trait remove -- remove trait from entity
8199 all_commands['trait_remove'] = Command(
8200 ("trait", "remove"), Id(help_ref="id:target:account"),
8201 SimpleString(help_ref="trait"),
8202 perm_filter="can_remove_trait")
8203 def trait_remove(self, operator, ety_id, trait_name):
8204 ety = self.util.get_target(ety_id, restrict_to=[])
8205 trait = self._get_constant(self.const.EntityTrait, trait_name, "trait")
8206 self.ba.can_remove_trait(operator.get_entity_id(), ety=ety, trait=trait)
8207
8208 if isinstance(ety, Utils.Factory.get('Disk')):
8209 ety_name = ety.path
8210 elif isinstance(ety, Utils.Factory.get('Person')):
8211 ety_name = ety.get_name(self.const.system_cached, self.const.name_full)
8212 else:
8213 ety_name = ety.get_names()[0][0]
8214 if ety.get_trait(trait) is None:
8215 return "%s has no %s trait" % (ety_name, trait)
8216 ety.delete_trait(trait)
8217 return "OK, deleted trait %s from %s" % (trait, ety_name)
8218
8219 # trait set -- add or update a trait
8220 all_commands['trait_set'] = Command(
8221 ("trait", "set"), Id(help_ref="id:target:account"),
8222 SimpleString(help_ref="trait"),
8223 SimpleString(help_ref="trait_val", repeat=True),
8224 perm_filter="can_set_trait")
8225 def trait_set(self, operator, ent_name, trait_name, *values):
8226 ent = self.util.get_target(ent_name, restrict_to=[])
8227 trait = self._get_constant(self.const.EntityTrait, trait_name, "trait")
8228 self.ba.can_set_trait(operator.get_entity_id(), trait=trait, ety=ent)
8229 params = {}
8230 for v in values:
8231 if v.count('='):
8232 key, value = v.split('=', 1)
8233 else:
8234 key = v; value = ''
8235 key = self.util.get_abbr_type(key, ('target_id', 'date', 'numval',
8236 'strval'))
8237 if value == '':
8238 params[key] = None
8239 elif key == 'target_id':
8240 target = self.util.get_target(value, restrict_to=[])
8241 params[key] = target.entity_id
8242 elif key == 'date':
8243 # TODO: _parse_date only handles dates, not hours etc.
8244 params[key] = self._parse_date(value)
8245 elif key == 'numval':
8246 params[key] = int(value)
8247 elif key == 'strval':
8248 params[key] = value
8249 ent.populate_trait(trait, **params)
8250 ent.write_db()
8251 return "Ok, set trait %s for %s" % (trait_name, ent_name)
8252
8253 # trait types -- list out the defined trait types
8254 all_commands['trait_types'] = Command(
8255 ("trait", "types"),
8256 fs=FormatSuggestion("%-25s %s", ('trait', 'description'),
8257 hdr="%-25s %s" % ('Trait', 'Description')),
8258 perm_filter="can_set_trait")
8259 def trait_types(self, operator):
8260 self.ba.can_set_trait(operator.get_entity_id())
8261 ret = [{"trait": str(x),
8262 "description": x.description}
8263 for x in self.const.fetch_constants(self.const.EntityTrait)]
8264 return sorted(ret, key=lambda x: x['trait'])
8265
8266 #
8267 # user commands
8268 #
8269
8270 # user affiliation_add
8271 all_commands['user_affiliation_add'] = Command(
8272 ("user", "affiliation_add"),
8273 AccountName(), OU(), Affiliation(), AffiliationStatus(),
8274 perm_filter='can_add_account_type')
8275 def user_affiliation_add(self, operator, accountname, ou, aff, aff_status):
8276 account = self._get_account(accountname)
8277 person = self._get_person('entity_id', account.owner_id)
8278 ou, aff, aff_status = self._person_affiliation_add_helper(
8279 operator, person, ou, aff, aff_status)
8280 self.ba.can_add_account_type(operator.get_entity_id(), account,
8281 ou, aff, aff_status)
8282 account.set_account_type(ou.entity_id, aff)
8283
8284 # When adding an affiliation manually, make sure the user gets
8285 # the e-mail addresses associated with it automatically. To
8286 # achieve this, we temporarily change the priority to 1 and
8287 # call write_db. This will displace an existing priority 1 if
8288 # there is one, but it's not worthwhile to do this perfectly.
8289 for row in account.get_account_types(filter_expired=False):
8290 if row['ou_id'] == ou.entity_id and row['affiliation'] == aff:
8291 priority = row['priority']
8292 break
8293 account.set_account_type(ou.entity_id, aff, 1)
8294 account.write_db()
8295 account.set_account_type(ou.entity_id, aff, priority)
8296 account.write_db()
8297 return "OK, added %s@%s to %s" % (aff, self._format_ou_name(ou),
8298 accountname)
8299
8300 # user affiliation_remove
8301 all_commands['user_affiliation_remove'] = Command(
8302 ("user", "affiliation_remove"), AccountName(), OU(), Affiliation(),
8303 perm_filter='can_remove_account_type')
8304 def user_affiliation_remove(self, operator, accountname, ou, aff):
8305 account = self._get_account(accountname)
8306 aff = self._get_affiliationid(aff)
8307 ou = self._get_ou(stedkode=ou)
8308 self.ba.can_remove_account_type(operator.get_entity_id(),
8309 account, ou, aff)
8310 account.del_account_type(ou.entity_id, aff)
8311 account.write_db()
8312 return "OK, removed %s@%s from %s" % (aff, self._format_ou_name(ou),
8313 accountname)
8314
8315 all_commands['user_create_unpersonal'] = Command(
8316 ('user', 'create_unpersonal'),
8317 AccountName(), GroupName(), EmailAddress(),
8318 SimpleString(help_ref="string_np_type"),
8319 fs=FormatSuggestion("Created account_id=%i", ("account_id",)),
8320 perm_filter='is_superuser')
8321
8322 def user_create_unpersonal(self, operator, account_name, group_name,
8323 contact_address, account_type):
8324 if not self.ba.is_superuser(operator.get_entity_id()):
8325 raise PermissionDenied("Only superusers may reserve users")
8326 account_type = self._get_constant(self.const.Account, account_type,
8327 "account type")
8328 account = self.Account_class(self.db)
8329 account.clear()
8330 account.populate(account_name,
8331 self.const.entity_group,
8332 self._get_group(group_name).entity_id,
8333 account_type,
8334 operator.get_entity_id(),
8335 None)
8336 account.write_db()
8337 passwd = account.make_passwd(account_name)
8338 account.set_password(passwd)
8339 try:
8340 account.write_db()
8341 except self.db.DatabaseError, m:
8342 raise CerebrumError("Database error: %s" % m)
8343
8344 if hasattr(self, 'entity_contactinfo_add'):
8345 self.entity_contactinfo_add(operator, account_name, 'EMAIL',
8346 contact_address)
8347 if hasattr(self, 'email_create_forward_target'):
8348 self.email_create_forward_target(
8349 operator,
8350 '{}@{}'.format(
8351 account_name,
8352 cereconf.EMAIL_DEFAULT_DOMAIN),
8353 contact_address)
8354
8355 operator.store_state("new_account_passwd",
8356 {'account_id': int(account.entity_id),
8357 'password': passwd})
8358 return {'account_id': int(account.entity_id)}
8359
8360 def _user_create_prompt_func(self, session, *args):
8361 """A prompt_func on the command level should return
8362 {'prompt': message_string, 'map': dict_mapping}
8363 - prompt is simply shown.
8364 - map (optional) maps the user-entered value to a value that
8365 is returned to the server, typically when user selects from
8366 a list."""
8367 all_args = list(args[:])
8368
8369 if not all_args:
8370 return {'prompt': 'Person identification',
8371 'help_ref': 'user_create_person_id'}
8372 arg = all_args.pop(0)
8373 if not all_args:
8374 c = self._find_persons(arg)
8375 person_map = [(('%-8s %s', 'Id', 'Name'), None)]
8376 for i in range(len(c)):
8377 person = self._get_person('entity_id', c[i]['person_id'])
8378 person_map.append((
8379 ('%8i %s', int(c[i]['person_id']),
8380 person.get_name(self.const.system_cached,
8381 self.const.name_full)),
8382 int(c[i]['person_id'])))
8383 if not len(person_map) > 1:
8384 raise CerebrumError('No persons matched')
8385 return {'prompt': 'Choose person from list',
8386 'map': person_map,
8387 'help_ref': 'user_create_select_person'}
8388 owner_id = all_args.pop(0)
8389 person = self._get_person('entity_id', owner_id)
8390 existing_accounts = []
8391 account = self.Account_class(self.db)
8392 for r in account.list_accounts_by_owner_id(person.entity_id):
8393 account = self._get_account(r['account_id'], idtype='id')
8394 if account.expire_date:
8395 exp = account.expire_date.strftime('%Y-%m-%d')
8396 else:
8397 exp = '<not set>'
8398 existing_accounts.append('%-10s %s' % (account.account_name,
8399 exp))
8400 if existing_accounts:
8401 existing_accounts = 'Existing accounts:\n%-10s %s\n%s\n' % (
8402 'uname', 'expire', '\n'.join(existing_accounts))
8403 else:
8404 existing_accounts = ''
8405 if existing_accounts:
8406 if not all_args:
8407 return {'prompt': '%sContinue? (y/n)' % existing_accounts}
8408 yes_no = all_args.pop(0)
8409 if not yes_no == 'y':
8410 raise CerebrumError('Command aborted at user request')
8411 if not all_args:
8412 aff_map = [(('%-8s %s', 'Num', 'Affiliation'), None)]
8413 for aff in person.get_affiliations():
8414 ou = self._get_ou(ou_id=aff['ou_id'])
8415 name = '%s@%s' % (
8416 self.const.PersonAffStatus(aff['status']),
8417 self._format_ou_name(ou))
8418 aff_map.append((('%s', name),
8419 {'ou_id': int(aff['ou_id']),
8420 'aff': int(aff['affiliation'])}))
8421 if not len(aff_map) > 1:
8422 raise CerebrumError('Person has no affiliations.')
8423 return {'prompt': 'Choose affiliation from list', 'map': aff_map}
8424 all_args.pop(0) # affiliation =
8425 if not all_args:
8426 return {'prompt': 'Shell', 'default': 'bash'}
8427 all_args.pop(0) # shell =
8428 if not all_args:
8429 return {'prompt': 'Disk', 'help_ref': 'disk'}
8430 all_args.pop(0) # disk =
8431 if not all_args:
8432 ret = {'prompt': 'Username', 'last_arg': True}
8433 posix_user = Utils.Factory.get('PosixUser')(self.db)
8434 try:
8435 person = self._get_person('entity_id', owner_id)
8436 fname, lname = [
8437 person.get_name(self.const.system_cached, v)
8438 for v in (self.const.name_first,
8439 self.const.name_last)]
8440 sugg = posix_user.suggest_unames(
8441 self.const.account_namespace, fname, lname)
8442 if sugg:
8443 ret['default'] = sugg[0]
8444 except ValueError:
8445 pass # Failed to generate a default username
8446 return ret
8447 if len(all_args) == 1:
8448 return {'last_arg': True}
8449 raise CerebrumError('Too many arguments')
8450
8451 all_commands['user_create_personal'] = Command(
8452 ('user', 'create_personal'), prompt_func=_user_create_prompt_func,
8453 fs=FormatSuggestion("Created uid=%i", ("uid",)),
8454 perm_filter='can_create_user')
8455
8456 def user_create_personal(self, operator, *args):
8457 if len(args) == 6:
8458 idtype, person_id, affiliation, shell, home, uname = args
8459 else:
8460 idtype, person_id, yes_no, affiliation, shell, home, uname = args
8461 owner_type = self.const.entity_person
8462 owner_id = self._get_person('entity_id', person_id).entity_id
8463 np_type = None
8464
8465 # Only superusers should be allowed to create users with
8466 # capital letters in their ids, and even then, just for system
8467 # users
8468 if uname != uname.lower():
8469 if (not self.ba.is_superuser(operator.get_entity_id()) and
8470 owner_type != self.const.entity_group):
8471 raise CerebrumError(
8472 'Personal account names cannot contain '
8473 'capital letters')
8474
8475 posix_user = Utils.Factory.get('PosixUser')(self.db)
8476 uid = posix_user.get_free_uid()
8477 shell = self._get_shell(shell)
8478 if home[0] != ':': # Hardcoded path
8479 disk_id, home = self._get_disk(home)[1:3]
8480 else:
8481 if not self.ba.is_superuser(operator.get_entity_id()):
8482 raise PermissionDenied(
8483 'Only superusers may use hardcoded path')
8484 disk_id, home = None, home[1:]
8485 posix_user.clear()
8486 gecos = None
8487 expire_date = None
8488 self.ba.can_create_user(operator.get_entity_id(), owner_id, disk_id)
8489
8490 posix_user.populate(uid, None, gecos, shell, name=uname,
8491 owner_type=owner_type,
8492 owner_id=owner_id, np_type=np_type,
8493 creator_id=operator.get_entity_id(),
8494 expire_date=expire_date)
8495 try:
8496 posix_user.write_db()
8497 for spread in cereconf.BOFHD_NEW_USER_SPREADS:
8498 posix_user.add_spread(self.const.Spread(spread))
8499 homedir_id = posix_user.set_homedir(
8500 disk_id=disk_id, home=home,
8501 status=self.const.home_status_not_created)
8502 posix_user.set_home(self.const.spread_uit_nis_user, homedir_id)
8503 # For correct ordering of ChangeLog events, new users
8504 # should be signalled as "exported to" a certain system
8505 # before the new user's password is set. Such systems are
8506 # flawed, and should be fixed.
8507 passwd = posix_user.make_passwd(uname)
8508 posix_user.set_password(passwd)
8509 # And, to write the new password to the database, we have
8510 # to .write_db() one more time...
8511 posix_user.write_db()
8512 if len(args) != 5:
8513 ou_id, affiliation = affiliation['ou_id'], affiliation['aff']
8514 self._user_create_set_account_type(posix_user, owner_id,
8515 ou_id, affiliation)
8516 except self.db.DatabaseError, m:
8517 raise CerebrumError('Database error: {}'.format(m))
8518 operator.store_state('new_account_passwd',
8519 {'account_id': int(posix_user.entity_id),
8520 'password': passwd})
8521 return {'uid': uid}
8522
8523 all_commands['user_reserve_personal'] = Command(
8524 ('user', 'reserve_personal'),
8525 PersonId(), AccountName(),
8526 fs=FormatSuggestion('Created account_id=%i', ('account_id',)),
8527 perm_filter='is_superuser')
8528
8529 def user_reserve_personal(self, operator, *args):
8530 person_id, uname = args
8531
8532 person = self._get_person(*self._map_person_id(person_id))
8533
8534 account = self.Account_class(self.db)
8535 account.clear()
8536 if not self.ba.is_superuser(operator.get_entity_id()):
8537 raise PermissionDenied('Only superusers may reserve users')
8538 account.populate(uname,
8539 self.const.entity_person,
8540 person.entity_id,
8541 None,
8542 operator.get_entity_id(),
8543 None)
8544 account.write_db()
8545 passwd = account.make_passwd(uname)
8546 account.set_password(passwd)
8547 try:
8548 account.write_db()
8549 except self.db.DatabaseError, m:
8550 raise CerebrumError('Database error: {}'.format(m))
8551 operator.store_state('new_account_passwd',
8552 {'account_id': int(account.entity_id),
8553 'password': passwd})
8554 return {'account_id': int(account.entity_id)}
8555
8556 all_commands['user_create_sysadm'] = Command(
8557 ("user", "create_sysadm"), AccountName(), OU(optional=True),
8558 fs=FormatSuggestion('OK, created %s', ('accountname',)),
8559 perm_filter='is_superuser')
8560 def user_create_sysadm(self, operator, accountname, stedkode=None):
8561 """ Create a sysadm account with the given accountname.
8562
8563 TBD, requirements?
8564 - Will add the person's primary affiliation, which must be
8565 of type ANSATT/tekadm.
8566
8567 :param str accountname:
8568 Account to be created. Must include a hyphen and end with one of
8569 SYSADM_TYPES.
8570
8571 :param str stedkode:
8572 Optional stedkode to place the sysadm account. Only used if a
8573 person have multipile valid affiliations.
8574
8575 """
8576 SYSADM_TYPES = ('adm','drift','null',)
8577 VALID_STATUS = (self.const.affiliation_status_ansatt_tekadm,
8578 self.const.affiliation_status_ansatt_vitenskapelig)
8579 DOMAIN = '@ulrik.uit.no'
8580
8581 if not self.ba.is_superuser(operator.get_entity_id()):
8582 raise PermissionDenied('Only superuser can create sysadm accounts')
8583 res = re.search('^([a-z]+)-([a-z]+)$', accountname)
8584 if res is None:
8585 raise CerebrumError('Username must be on the form "foo-adm"')
8586 user, suffix = res.groups()
8587 if suffix not in SYSADM_TYPES:
8588 raise CerebrumError(
8589 'Username "%s" does not have one of these suffixes: %s' %
8590 (accountname, ', '.join(SYSADM_TYPES)))
8591 # Funky... better solutions?
8592 try:
8593 self._get_account(accountname)
8594 except CerebrumError:
8595 pass
8596 else:
8597 raise CerebrumError('Username already in use')
8598 account_owner = self._get_account(user)
8599 if account_owner.owner_type != self.const.entity_person:
8600 raise CerebrumError('Can only create personal sysadm accounts')
8601 person = self._get_person('account_name', user)
8602 if stedkode is not None:
8603 ou = self._get_ou(stedkode=stedkode)
8604 ou_id = ou.entity_id
8605 else:
8606 ou_id = None
8607 valid_aff = person.list_affiliations(person_id=person.entity_id,
8608 source_system=self.const.system_sap,
8609 status=VALID_STATUS,
8610 ou_id=ou_id)
8611 status_blob = ', '.join(map(str,VALID_STATUS))
8612 if valid_aff == []:
8613 raise CerebrumError('Person has no %s affiliation' % status_blob)
8614 elif len(valid_aff) > 1:
8615 raise CerebrumError('More than than one %s affiliation, '
8616 'add stedkode as argument' % status_blob)
8617 self.user_reserve_personal(operator, 'entity_id:{}'.format(person.entity_id), accountname)
8618 self._user_create_set_account_type(self._get_account(accountname),
8619 person.entity_id,
8620 valid_aff[0]['ou_id'],
8621 valid_aff[0]['affiliation'])
8622 self.trait_set(operator, accountname, 'sysadm_account', 'strval=on')
8623 self.user_promote_posix(operator, accountname, shell='bash', home=':/')
8624 account = self._get_account(accountname)
8625 account.add_spread(self.const.spread_uit_ad_account)
8626 self.entity_contactinfo_add(operator, accountname, 'EMAIL', user+DOMAIN)
8627 self.email_create_forward_target(operator, accountname+DOMAIN, user+DOMAIN)
8628 return {'accountname': accountname}
8629
8630
8631 def _check_for_pipe_run_as(self, account_id):
8632 et = Email.EmailTarget(self.db)
8633 try:
8634 et.clear()
8635 et.find_by_email_target_attrs(target_type=self.const.email_target_pipe,
8636 using_uid=account_id)
8637 except Errors.NotFoundError:
8638 return False
8639 except Errors.TooManyRowsError:
8640 return True
8641 return True
8642
8643 # user delete
8644 all_commands['user_delete'] = Command(
8645 ("user", "delete"), AccountName(), perm_filter='can_delete_user')
8646 def user_delete(self, operator, accountname):
8647 # TODO: How do we delete accounts?
8648 account = self._get_account(accountname)
8649 self.ba.can_delete_user(operator.get_entity_id(), account)
8650 if account.is_deleted():
8651 raise CerebrumError, "User is already deleted"
8652 if self._check_for_pipe_run_as(account.entity_id):
8653 raise CerebrumError, ("User is associated with an e-mail pipe " +
8654 "and cannot be deleted until the pipe is " +
8655 "removed. Please notify postmaster if you " +
8656 "are not able to remove the pipe yourself.")
8657
8658 # Here we'll register a bofhd_reguest to archive the content of the
8659 # users home directory.
8660 br = BofhdRequests(self.db, self.const)
8661 br.add_request(operator.get_entity_id(), br.now,
8662 self.const.bofh_delete_user,
8663 account.entity_id, None,
8664 state_data=int(self.const.spread_uit_nis_user))
8665 return "User %s queued for deletion immediately" % account.account_name
8666
8667 all_commands['user_set_disk_quota'] = Command(
8668 ("user", "set_disk_quota"), AccountName(), Integer(help_ref="disk_quota_size"),
8669 Date(help_ref="disk_quota_expire_date"), SimpleString(help_ref="string_why"),
8670 perm_filter='can_set_disk_quota')
8671 def user_set_disk_quota(self, operator, accountname, size, date, why):
8672 account = self._get_account(accountname)
8673 try:
8674 age = DateTime.strptime(date, '%Y-%m-%d') - DateTime.now()
8675 except:
8676 raise CerebrumError, "Error parsing date"
8677 why = why.strip()
8678 if len(why) < 3:
8679 raise CerebrumError, "Why cannot be blank"
8680 unlimited = forever = False
8681 if age.days > 185:
8682 forever = True
8683 try:
8684 size = int(size)
8685 except ValueError:
8686 raise CerebrumError, "Expected int as size"
8687 if size > 1024 or size < 0: # "unlimited" for perm-check = +1024M
8688 unlimited = True
8689 self.ba.can_set_disk_quota(operator.get_entity_id(), account,
8690 unlimited=unlimited, forever=forever)
8691 home = account.get_home(self.const.spread_uit_nis_user)
8692 _date = self._parse_date(date)
8693 if size < 0: # Unlimited
8694 size = None
8695 dq = DiskQuota(self.db)
8696 dq.set_quota(home['homedir_id'], override_quota=size,
8697 override_expiration=_date, description=why)
8698 return "OK, quota overridden for %s" % accountname
8699
8700 # user gecos
8701 all_commands['user_gecos'] = Command(
8702 ("user", "gecos"), AccountName(), PosixGecos(),
8703 perm_filter='can_set_gecos')
8704 def user_gecos(self, operator, accountname, gecos):
8705 account = self._get_account(accountname, actype="PosixUser")
8706 # Set gecos to NULL if user requests a whitespace-only string.
8707 self.ba.can_set_gecos(operator.get_entity_id(), account)
8708 # TBD: Should we allow 8-bit characters?
8709 try:
8710 gecos.encode("ascii")
8711 except UnicodeDecodeError:
8712 raise CerebrumError, "GECOS can only contain US-ASCII."
8713 account.gecos = gecos.strip() or None
8714 account.write_db()
8715 # TBD: As the 'gecos' attribute lives in class PosixUser,
8716 # which is ahead of AccountEmailMixin in the MRO of 'account',
8717 # the write_db() method of AccountEmailMixin will receive a
8718 # "no updates happened" from its call to superclasses'
8719 # write_db(). Is there a better way to solve this kind of
8720 # problem than by adding explicit calls to one if the mixin's
8721 # methods? The following call will break if anyone tries this
8722 # code with an Email-less cereconf.CLASS_ACCOUNT.
8723 account.update_email_addresses()
8724 return "OK, set gecos for %s to '%s'" % (accountname, gecos)
8725
8726 # filtered user History
8727 all_commands['user_history_filtered'] = Command(
8728 ("user", "history"), AccountName(),
8729 perm_filter='can_show_history')
8730 def user_history_filtered(self, operator,accountname):
8731 self.logger.warn("in user history filtered")
8732 account = self._get_account(accountname)
8733 self.ba.can_show_history(operator.get_entity_id(), account)
8734 ret = []
8735 timedelta = "%s" % (DateTime.mxDateTime.now() - DateTime.DateTimeDelta(7))
8736 timeperiod = timedelta.split(" ")
8737
8738 for r in self.db.get_log_events(0, subject_entity=account.entity_id,sdate=timeperiod[0]):
8739 ret.append(self._format_changelog_entry(r))
8740 ret_val = ""
8741 for item in ret:
8742 ret_val +="\n"
8743 for key,value in item.iteritems():
8744 ret_val+="%s\t" % str(value)
8745 return ret_val
8746
8747 # user history
8748 all_commands['user_history'] = Command(
8749 ("user", "history"), AccountName(),
8750 fs=FormatSuggestion("%s [%s]: %s",
8751 ("timestamp", "change_by", "message")),
8752 perm_filter='can_show_history')
8753 def user_history(self, operator, accountname):
8754 return self.entity_history(operator, accountname)
8755
8756 # user info
8757 all_commands['user_info'] = Command(
8758 ("user", "info"), AccountName(),
8759 fs=FormatSuggestion([("Username: %s\n"+
8760 "Spreads: %s\n" +
8761 "Affiliations: %s\n" +
8762 "Expire: %s\n" +
8763 "Home: %s (status: %s)\n" +
8764 "Entity id: %i\n" +
8765 "Owner id: %i (%s: %s)",
8766 ("username", "spread", "affiliations",
8767 format_day("expire"),
8768 "home", "home_status", "entity_id", "owner_id",
8769 "owner_type", "owner_desc")),
8770 ("Disk quota: %s MiB",
8771 ("disk_quota",)),
8772 ("DQ override: %s MiB (until %s: %s)",
8773 ("dq_override", format_day("dq_expire"), "dq_why")),
8774 ("UID: %i\n" +
8775 "Default fg: %i=%s\n" +
8776 "Gecos: %s\n" +
8777 "Shell: %s",
8778 ('uid', 'dfg_posix_gid', 'dfg_name', 'gecos',
8779 'shell')),
8780 ("Quarantined: %s",
8781 ("quarantined",))]))
8782 def user_info(self, operator, accountname):
8783 is_posix = False
8784 try:
8785 account = self._get_account(accountname, actype="PosixUser")
8786 is_posix = True
8787 except CerebrumError:
8788 account = self._get_account(accountname)
8789 if account.is_deleted() and not self.ba.is_superuser(operator.get_entity_id()):
8790 raise CerebrumError("User is deleted")
8791 affiliations = []
8792 for row in account.get_account_types(filter_expired=False):
8793 ou = self._get_ou(ou_id=row['ou_id'])
8794 affiliations.append("%s@%s" %
8795 (self.const.PersonAffiliation(row['affiliation']),
8796 self._format_ou_name(ou)))
8797 tmp = {'disk_id': None, 'home': None, 'status': None,
8798 'homedir_id': None}
8799 home_status = None
8800 spread = 'spread_uit_nis_user'
8801 if spread in cereconf.HOME_SPREADS:
8802 try:
8803 tmp = account.get_home(getattr(self.const, spread))
8804 home_status = str(self.const.AccountHomeStatus(tmp['status']))
8805 except Errors.NotFoundError:
8806 pass
8807
8808 ret = {'entity_id': account.entity_id,
8809 'username': account.account_name,
8810 'spread': ",".join([str(self.const.Spread(a['spread']))
8811 for a in account.get_spread()]),
8812 'affiliations': (",\n" + (" " * 15)).join(affiliations),
8813 'expire': account.expire_date,
8814 'home_status': home_status,
8815 'owner_id': account.owner_id,
8816 'owner_type': str(self.const.EntityType(account.owner_type))
8817 }
8818 try:
8819 self.ba.can_show_disk_quota(operator.get_entity_id(), account)
8820 can_see_quota = True
8821 except PermissionDenied:
8822 can_see_quota = False
8823 if tmp['disk_id'] and can_see_quota:
8824 disk = Utils.Factory.get("Disk")(self.db)
8825 disk.find(tmp['disk_id'])
8826 def_quota = disk.get_default_quota()
8827 try:
8828 dq = DiskQuota(self.db)
8829 dq_row = dq.get_quota(tmp['homedir_id'])
8830 if not(dq_row['quota'] is None or def_quota is False):
8831 ret['disk_quota'] = str(dq_row['quota'])
8832 # Only display recent quotas
8833 days_left = ((dq_row['override_expiration'] or DateTime.Epoch) -
8834 DateTime.now()).days
8835 if days_left > -30:
8836 ret['dq_override'] = dq_row['override_quota']
8837 if dq_row['override_quota'] is not None:
8838 ret['dq_override'] = str(dq_row['override_quota'])
8839 ret['dq_expire'] = dq_row['override_expiration']
8840 ret['dq_why'] = dq_row['description']
8841 if days_left < 0:
8842 ret['dq_why'] += " [INACTIVE]"
8843 except Errors.NotFoundError:
8844 if def_quota:
8845 ret['disk_quota'] = "(%s)" % def_quota
8846
8847 if account.owner_type == self.const.entity_person:
8848 person = self._get_person('entity_id', account.owner_id)
8849 try:
8850 p_name = person.get_name(self.const.system_cached,
8851 getattr(self.const,
8852 cereconf.DEFAULT_GECOS_NAME))
8853 except Errors.NotFoundError:
8854 p_name = '<none>'
8855 ret['owner_desc'] = p_name
8856 else:
8857 grp = self._get_group(account.owner_id, idtype='id')
8858 ret['owner_desc'] = grp.group_name
8859
8860 # home is not mandatory for some of the instances that "copy"
8861 # this user_info-method
8862 if tmp['disk_id'] or tmp['home']:
8863 ret['home'] = account.resolve_homedir(disk_id=tmp['disk_id'],
8864 home=tmp['home'])
8865 else:
8866 ret['home'] = None
8867 if is_posix:
8868 group = self._get_group(account.gid_id, idtype='id', grtype='PosixGroup')
8869 ret['uid'] = account.posix_uid
8870 ret['dfg_posix_gid'] = group.posix_gid
8871 ret['dfg_name'] = group.group_name
8872 ret['gecos'] = account.gecos
8873 ret['shell'] = str(self.const.PosixShell(account.shell))
8874 # TODO: Return more info about account
8875 quarantined = None
8876 now = DateTime.now()
8877 for q in account.get_entity_quarantine():
8878 if q['start_date'] <= now:
8879 if (q['end_date'] is not None and
8880 q['end_date'] < now):
8881 quarantined = 'expired'
8882 elif (q['disable_until'] is not None and
8883 q['disable_until'] > now):
8884 quarantined = 'disabled'
8885 else:
8886 quarantined = 'active'
8887 break
8888 else:
8889 quarantined = 'pending'
8890 if quarantined:
8891 ret['quarantined'] = quarantined
8892 return ret
8893
8894
8895 def _get_cached_passwords(self, operator):
8896 ret = []
8897 for r in operator.get_state():
8898 # state_type, entity_id, state_data, set_time
8899 if r['state_type'] in ('new_account_passwd', 'user_passwd'):
8900 ret.append({'account_id': self._get_entity_name(
8901 r['state_data']['account_id'],
8902 self.const.entity_account),
8903 'password': r['state_data']['password'],
8904 'operation': r['state_type']})
8905 return ret
8906
8907 all_commands['user_find'] = Command(
8908 ("user", "find"),
8909 UserSearchType(),
8910 SimpleString(),
8911 YesNo(optional=True, default='n', help_ref='yes_no_include_expired'),
8912 SimpleString(optional=True, help_ref="affiliation_optional"),
8913 fs=FormatSuggestion("%7i %-12s %s", ('entity_id', 'username',
8914 format_day("expire")),
8915 hdr="%7s %-10s %-12s" % ('Id', 'Username',
8916 'Expire date')))
8917
8918 def user_find(self, operator, search_type, value,
8919 include_expired="no", aff_filter=None):
8920 acc = self.Account_class(self.db)
8921 if aff_filter is not None:
8922 try:
8923 aff_filter = int(self.const.PersonAffiliation(aff_filter))
8924 except Errors.NotFoundError:
8925 raise CerebrumError, "Invalid affiliation %s" % aff_filter
8926 filter_expired = not self._get_boolean(include_expired)
8927
8928 if search_type == 'stedkode':
8929 ou = self._get_ou(stedkode=value)
8930 rows = acc.list_accounts_by_type(ou_id=ou.entity_id,
8931 affiliation=aff_filter,
8932 filter_expired=filter_expired)
8933 elif search_type == 'host':
8934 # FIXME: filtering on affiliation is not implemented
8935 host = self._get_host(value)
8936 rows = acc.list_account_home(host_id=int(host.entity_id),
8937 filter_expired=filter_expired)
8938 elif search_type == 'disk':
8939 # FIXME: filtering on affiliation is not implemented
8940 disk = self._get_disk(value)[0]
8941 rows = acc.list_account_home(disk_id=int(disk.entity_id),
8942 filter_expired=filter_expired)
8943 else:
8944 raise CerebrumError, "Unknown search type (%s)" % search_type
8945 seen = {}
8946 ret = []
8947 for r in rows:
8948 a = int(r['account_id'])
8949 if a in seen:
8950 continue
8951 seen[a] = True
8952 acc.clear()
8953 acc.find(a)
8954 ret.append({'entity_id': a,
8955 'expire': acc.expire_date,
8956 'username': acc.account_name})
8957 ret.sort(lambda x, y: cmp(x['username'], y['username']))
8958 return ret
8959
8960 # user move prompt
8961 def user_move_prompt_func(self, session, *args):
8962 u""" user move prompt helper
8963
8964 Base command:
8965 user move <move-type> <account-name>
8966 Variants
8967 user move immediate <account-name> <disk-id> <reason>
8968 user move batch <account-name> <disk-id> <reason>
8969 user move nofile <account-name> <disk-id> <reason>
8970 user move hard_nofile <account-name> <disk-id> <reason>
8971 user move request <account-name> <disk-id> <reason>
8972 user move give <account-name> <group-name> <reason>
8973
8974 """
8975 help_struct = Help([self, ], logger=self.logger)
8976 all_args = list(args)
8977 if not all_args:
8978 return MoveType().get_struct(help_struct)
8979 move_type = all_args.pop(0)
8980 if not all_args:
8981 return AccountName().get_struct(help_struct)
8982 # pop account name
8983 all_args.pop(0)
8984 if move_type in (
8985 "immediate", "batch", "nofile", "hard_nofile"):
8986 # move_type needs disk-id
8987 if not all_args:
8988 r = DiskId().get_struct(help_struct)
8989 r['last_arg'] = True
8990 return r
8991 return {'last_arg': True}
8992 elif move_type in (
8993 "student", "student_immediate", "confirm", "cancel"):
8994 # move_type doesnt need more args
8995 return {'last_arg': True}
8996 elif move_type in ("request",):
8997 # move_type needs disk-id and reason
8998 if not all_args:
8999 return DiskId().get_struct(help_struct)
9000 # pop disk id
9001 all_args.pop(0)
9002 if not all_args:
9003 r = SimpleString(help_ref="string_why").get_struct(help_struct)
9004 r['last_arg'] = True
9005 return r
9006 return {'last_arg': True}
9007 elif move_type in ("give",):
9008 # move_type needs group-name and reason
9009 if not all_args:
9010 return GroupName().get_struct(help_struct)
9011 # pop group-name
9012 all_args.pop(0)
9013 if not all_args:
9014 r = SimpleString(help_ref="string_why").get_struct(help_struct)
9015 r['last_arg'] = True
9016 return r
9017 return {'last_arg': True}
9018 raise CerebrumError("Bad user_move command ({!s})".format(move_type))
9019
9020 #
9021 # user move <move-type> <account-name> [opts]
9022 #
9023 all_commands['user_move'] = Command(
9024 ("user", "move"),
9025 prompt_func=user_move_prompt_func,
9026 perm_filter='can_move_user')
9027
9028 def user_move(self, operator, move_type, accountname, *args):
9029 """
9030 """
9031 # now strip all str / unicode arguments in order to please CRB-2172
9032 def strip_arg(arg):
9033 if isinstance(arg, basestring):
9034 return arg.strip()
9035 return arg
9036 args = tuple(map(strip_arg, args))
9037 self.logger.debug('user_move: after stripping args ({args})'.format(
9038 args=args))
9039 account = self._get_account(accountname)
9040 account_error = lambda reason: "Cannot move {!r}, {!s}".format(
9041 account.account_name, reason)
9042
9043 REQUEST_REASON_MAX_LEN = 80
9044
9045 def _check_reason(reason):
9046 if len(reason) > REQUEST_REASON_MAX_LEN:
9047 raise CerebrumError(
9048 "Too long explanation, "
9049 "maximum length is {:d}".format(REQUEST_REASON_MAX_LEN))
9050
9051 if account.is_expired():
9052 raise CerebrumError(account_error("account is expired"))
9053 br = BofhdRequests(self.db, self.const)
9054 spread = int(self.const.spread_uit_nis_user)
9055 if move_type in ("immediate", "batch", "student", "student_immediate",
9056 "request", "give"):
9057 try:
9058 ah = account.get_home(spread)
9059 except Errors.NotFoundError:
9060 raise CerebrumError(account_error("account has no home"))
9061 if move_type in ("immediate", "batch", "nofile"):
9062 message = ""
9063 disk, disk_id = self._get_disk(args[0])[:2]
9064 if disk_id is None:
9065 raise CerebrumError(account_error("bad destination disk"))
9066 self.ba.can_move_user(operator.get_entity_id(), account, disk_id)
9067
9068 for r in account.get_spread():
9069 if (r['spread'] == self.const.spread_ifi_nis_user
9070 and not re.match(r'^/ifi/', args[0])):
9071 message += ("WARNING: moving user with %s-spread to "
9072 "a non-Ifi disk.\n" %
9073 self.const.spread_ifi_nis_user)
9074 break
9075
9076 # Let's check the disk quota settings. We only give a an
9077 # information message, the actual change happens when
9078 # set_homedir is done.
9079 default_dest_quota = disk.get_default_quota()
9080 current_quota = None
9081 dq = DiskQuota(self.db)
9082 try:
9083 ah = account.get_home(spread)
9084 except Errors.NotFoundError:
9085 raise CerebrumError(account_error("account has no home"))
9086 try:
9087 dq_row = dq.get_quota(ah['homedir_id'])
9088 except Errors.NotFoundError:
9089 pass
9090 else:
9091 current_quota = dq_row['quota']
9092 if dq_row['quota'] is not None:
9093 current_quota = dq_row['quota']
9094 days_left = ((dq_row['override_expiration'] or
9095 DateTime.Epoch) - DateTime.now()).days
9096 if days_left > 0 and dq_row['override_quota'] is not None:
9097 current_quota = dq_row['override_quota']
9098
9099 if current_quota is None:
9100 # this is OK
9101 pass
9102 elif default_dest_quota is False:
9103 message += ("Destination disk has no quota, so the current "
9104 "quota (%d) will be cleared.\n" % current_quota)
9105 elif current_quota <= default_dest_quota:
9106 message += ("Current quota (%d) is smaller or equal to the "
9107 "default at destination (%d), so it will be "
9108 "removed.\n") % (current_quota, default_dest_quota)
9109
9110 if move_type == "immediate":
9111 br.add_request(operator.get_entity_id(), br.now,
9112 self.const.bofh_move_user_now,
9113 account.entity_id, disk_id, state_data=spread)
9114 message += "Command queued for immediate execution."
9115 elif move_type == "batch":
9116 br.add_request(operator.get_entity_id(), br.batch_time,
9117 self.const.bofh_move_user,
9118 account.entity_id, disk_id, state_data=spread)
9119 message += ("Move queued for execution at %s." %
9120 self._date_human_readable(br.batch_time))
9121 # mail user about the awaiting move operation
9122 new_homedir = disk.path + '/' + account.account_name
9123 try:
9124 Utils.mail_template(
9125 account.get_primary_mailaddress(),
9126 cereconf.USER_BATCH_MOVE_WARNING,
9127 substitute={'USER': account.account_name,
9128 'TO_DISK': new_homedir})
9129 except Exception as e:
9130 self.logger.info("Sending mail failed: %s", e)
9131 elif move_type == "nofile":
9132 ah = account.get_home(spread)
9133 account.set_homedir(current_id=ah['homedir_id'],
9134 disk_id=disk_id)
9135 account.write_db()
9136 message += "User moved."
9137 return message
9138 elif move_type in ("hard_nofile",):
9139 if not self.ba.is_superuser(operator.get_entity_id()):
9140 raise PermissionDenied("only superusers may use hard_nofile")
9141 ah = account.get_home(spread)
9142 account.set_homedir(current_id=ah['homedir_id'], home=args[0])
9143 return "OK, user moved to hardcoded homedir"
9144 elif move_type in (
9145 "student", "student_immediate", "confirm", "cancel"):
9146 self.ba.can_give_user(operator.get_entity_id(), account)
9147 if move_type == "student":
9148 br.add_request(operator.get_entity_id(), br.batch_time,
9149 self.const.bofh_move_student,
9150 account.entity_id, None, state_data=spread)
9151 return ("student-move queued for execution at %s" %
9152 self._date_human_readable(br.batch_time))
9153 elif move_type == "student_immediate":
9154 br.add_request(operator.get_entity_id(), br.now,
9155 self.const.bofh_move_student,
9156 account.entity_id, None, state_data=spread)
9157 return "student-move queued for immediate execution"
9158 elif move_type == "confirm":
9159 r = br.get_requests(entity_id=account.entity_id,
9160 operation=self.const.bofh_move_request)
9161 if not r:
9162 raise CerebrumError("No matching request found")
9163 br.delete_request(account.entity_id,
9164 operation=self.const.bofh_move_request)
9165 # Flag as authenticated
9166 br.add_request(operator.get_entity_id(), br.batch_time,
9167 self.const.bofh_move_user,
9168 account.entity_id, r[0]['destination_id'],
9169 state_data=spread)
9170 return ("move queued for execution at %s" %
9171 self._date_human_readable(br.batch_time))
9172 elif move_type == "cancel":
9173 # TBD: Should superuser delete other request types as well?
9174 count = 0
9175 for tmp in br.get_requests(entity_id=account.entity_id):
9176 if tmp['operation'] in (
9177 self.const.bofh_move_student,
9178 self.const.bofh_move_user,
9179 self.const.bofh_move_give,
9180 self.const.bofh_move_request,
9181 self.const.bofh_move_user_now):
9182 count += 1
9183 br.delete_request(request_id=tmp['request_id'])
9184 return "OK, %i bofhd requests deleted" % count
9185 elif move_type in ("request",):
9186 disk = args[0]
9187 why = args[1]
9188 disk_id = self._get_disk(disk)[1]
9189 _check_reason(why)
9190 self.ba.can_receive_user(
9191 operator.get_entity_id(), account, disk_id)
9192 br.add_request(operator.get_entity_id(), br.now,
9193 self.const.bofh_move_request,
9194 account.entity_id, disk_id, why)
9195 return "OK, request registered"
9196 elif move_type in ("give",):
9197 self.ba.can_give_user(operator.get_entity_id(), account)
9198 group = args[0]
9199 why = args[1]
9200 group = self._get_group(group)
9201 _check_reason(why)
9202 br.add_request(operator.get_entity_id(), br.now,
9203 self.const.bofh_move_give,
9204 account.entity_id, group.entity_id, why)
9205 return "OK, 'give' registered"
9206
9207 #
9208 # user password
9209 #
9210 all_commands['user_password'] = Command(
9211 ('user', 'password'),
9212 AccountName(),
9213 AccountPassword(optional=True))
9214
9215 def user_password(self, operator, accountname, password=None):
9216 account = self._get_account(accountname)
9217 self.ba.can_set_password(operator.get_entity_id(), account)
9218 if password is None:
9219 password = account.make_passwd(accountname)
9220 else:
9221 # this is a bit complicated, but the point is that
9222 # superusers are allowed to *specify* passwords for other
9223 # users if cereconf.BOFHD_SU_CAN_SPECIFY_PASSWORDS=True
9224 # otherwise superusers may change passwords by assigning
9225 # automatic passwords only.
9226 if self.ba.is_superuser(operator.get_entity_id()):
9227 if (operator.get_entity_id() != account.entity_id and
9228 not cereconf.BOFHD_SU_CAN_SPECIFY_PASSWORDS):
9229 raise CerebrumError("Superuser cannot specify passwords "
9230 "for other users")
9231 elif operator.get_entity_id() != account.entity_id:
9232 raise CerebrumError(
9233 "Cannot specify password for another user.")
9234 try:
9235 check_password(password, account, structured=False)
9236 except RigidPasswordNotGoodEnough as e:
9237 raise CerebrumError('Bad password: {err_msg}'.format(
9238 err_msg=str(e).decode('utf-8').encode('latin-1')))
9239 except PhrasePasswordNotGoodEnough as e:
9240 raise CerebrumError('Bad passphrase: {err_msg}'.format(
9241 err_msg=str(e).decode('utf-8').encode('latin-1')))
9242 except PasswordNotGoodEnough as e:
9243 raise CerebrumError('Bad password: {err_msg}'.format(err_msg=e))
9244 account.set_password(password)
9245 account.write_db()
9246 operator.store_state("user_passwd",
9247 {'account_id': int(account.entity_id),
9248 'password': password})
9249 # Remove "weak password" quarantine
9250 for r in account.get_entity_quarantine():
9251 if int(r['quarantine_type']) == self.const.quarantine_autopassord:
9252 account.delete_entity_quarantine(
9253 self.const.quarantine_autopassord)
9254
9255 if int(r['quarantine_type']) == self.const.quarantine_svakt_passord:
9256 account.delete_entity_quarantine(
9257 self.const.quarantine_svakt_passord)
9258
9259 if account.is_deleted():
9260 return "OK. Warning: user is deleted"
9261 elif account.is_expired():
9262 return "OK. Warning: user is expired"
9263 elif account.get_entity_quarantine(only_active=True):
9264 return "OK. Warning: user has an active quarantine"
9265 return ("Password altered. Please use misc list_passwords to view the "
9266 "new password, or misc print_passwords to print password "
9267 "letters.")
9268
9269 # user promote_posix
9270 all_commands['user_promote_posix'] = Command(
9271 ('user', 'promote_posix'), AccountName(),
9272 PosixShell(default="bash"), DiskId(),
9273 perm_filter='can_create_user')
9274 def user_promote_posix(self, operator, accountname, shell=None, home=None):
9275 is_posix = False
9276 try:
9277 self._get_account(accountname, actype="PosixUser")
9278 is_posix = True
9279 except CerebrumError:
9280 pass
9281 if is_posix:
9282 raise CerebrumError("%s is already a PosixUser" % accountname)
9283 account = self._get_account(accountname)
9284 pu = Utils.Factory.get('PosixUser')(self.db)
9285 old_uid = self._lookup_old_uid(account.entity_id)
9286 if old_uid is None:
9287 uid = pu.get_free_uid()
9288 else:
9289 uid = old_uid
9290 shell = self._get_shell(shell)
9291 if not home:
9292 raise CerebrumError("home cannot be empty")
9293 elif home[0] != ':': # Hardcoded path
9294 disk_id, home = self._get_disk(home)[1:3]
9295 else:
9296 if not self.ba.is_superuser(operator.get_entity_id()):
9297 raise PermissionDenied("only superusers may use hardcoded path")
9298 disk_id, home = None, home[1:]
9299 if account.owner_type == self.const.entity_person:
9300 person = self._get_person("entity_id", account.owner_id)
9301 else:
9302 person = None
9303 self.ba.can_create_user(operator.get_entity_id(), person, disk_id)
9304 pu.populate(uid, None, None, shell, parent=account,
9305 creator_id=operator.get_entity_id())
9306 pu.write_db()
9307
9308 default_home_spread = self._get_constant(self.const.Spread,
9309 cereconf.DEFAULT_HOME_SPREAD,
9310 "spread")
9311 if not pu.has_spread(default_home_spread):
9312 pu.add_spread(default_home_spread)
9313
9314 homedir_id = pu.set_homedir(
9315 disk_id=disk_id, home=home,
9316 status=self.const.home_status_not_created)
9317 pu.set_home(default_home_spread, homedir_id)
9318 if old_uid is None:
9319 tmp = ', new uid=%i' % uid
9320 else:
9321 tmp = ', reused old uid=%i' % old_uid
9322 return "OK, promoted %s to posix user%s" % (accountname, tmp)
9323
9324 # user posix_delete
9325 all_commands['user_demote_posix'] = Command(
9326 ('user', 'demote_posix'), AccountName(), perm_filter='can_create_user')
9327 def user_demote_posix(self, operator, accountname):
9328 if not self.ba.is_superuser(operator.get_entity_id()):
9329 raise PermissionDenied("currently limited to superusers")
9330 user = self._get_account(accountname, actype="PosixUser")
9331 user.delete_posixuser()
9332 return "OK, %s was demoted" % accountname
9333
9334 def user_restore_prompt_func(self, session, *args):
9335 '''Helper function for user_restore. Will display a prompt that
9336 asks which affiliation should be used, and more..'''
9337
9338 all_args = list(args[:])
9339
9340 # Get the account name
9341 if not all_args:
9342 return {'prompt': 'Account name',
9343 'help_ref': 'account_name'}
9344 arg = all_args.pop(0)
9345 ac = self._get_account(arg)
9346
9347 # Print a list of affiliations registred on the accounts owner (person)
9348 # Prompts user to select one of these. Checks if the input is sane.
9349 if not all_args:
9350 person = self._get_person('entity_id', ac.owner_id)
9351 map = [(('%-8s %s', 'Num', 'Affiliation'), None)]
9352 for aff in person.get_affiliations():
9353 ou = self._get_ou(ou_id=aff['ou_id'])
9354 name = '%s@%s' % (self.const.PersonAffStatus(aff['status']),
9355 self._format_ou_name(ou))
9356 map.append((('%s', name), {'ou_id': int(aff['ou_id']),
9357 'aff': int(aff['affiliation'])}))
9358 if not len(map) > 1:
9359 raise CerebrumError('Person has no affiliations.')
9360 return {'prompt': 'Choose affiliation from list', 'map': map}
9361 arg = all_args.pop(0)
9362 if isinstance(arg, type({})) and arg.has_key('aff') and \
9363 arg.has_key('ou_id'):
9364 ou = arg['ou_id']
9365 aff = arg['aff']
9366 else:
9367 raise CerebrumError('Invalid affiliation')
9368
9369 # Gets the disk the user will reside on
9370 if not all_args:
9371 return {'prompt': 'Disk',
9372 'help_ref': 'disk',
9373 'last_arg': True}
9374 arg = all_args.pop(0)
9375 disk = self._get_disk(arg)
9376
9377 # Finishes off
9378 if len(all_args) == 0:
9379 return {'last_arg': True}
9380
9381 # We'll raise an error, if there is too many arguments:
9382 raise CerebrumError('Too many arguments')
9383
9384 # user restore
9385 all_commands['user_restore'] = Command(
9386 ('user', 'restore'), prompt_func=user_restore_prompt_func,
9387 perm_filter='can_create_user')
9388 def user_restore(self, operator, accountname, aff_ou, home):
9389 ac = self._get_account(accountname)
9390 # Check if the account is deleted or reserved
9391 if not ac.is_deleted() and not ac.is_reserved():
9392 raise CerebrumError, \
9393 ('Please contact brukerreg in order to restore %s'
9394 % accountname)
9395
9396 # Checking to see if the home path is hardcoded.
9397 # Raises CerebrumError if the disk does not exist.
9398 if not home:
9399 raise CerebrumError('Home must be specified')
9400 elif home[0] != ':': # Hardcoded path
9401 disk_id, home = self._get_disk(home)[1:3]
9402 else:
9403 if not self.ba.is_superuser(operator.get_entity_id()):
9404 raise PermissionDenied('Only superusers may use hardcoded path')
9405 disk_id, home = None, home[1:]
9406
9407 # Check if the operator can alter the user
9408 if not self.ba.can_create_user(operator.get_entity_id(),
9409 ac, disk_id):
9410 raise PermissionDenied('User restore is limited')
9411
9412 # We demote posix
9413 try:
9414 pu = self._get_account(accountname, actype='PosixUser')
9415 except CerebrumError:
9416 pu = Utils.Factory.get('PosixUser')(self.db)
9417 else:
9418 pu.delete_posixuser()
9419 pu = Utils.Factory.get('PosixUser')(self.db)
9420
9421 # We remove all old group memberships
9422 grp = self.Group_class(self.db)
9423 for row in grp.search(member_id=ac.entity_id):
9424 grp.clear()
9425 grp.find(row['group_id'])
9426 grp.remove_member(ac.entity_id)
9427 grp.write_db()
9428
9429 # We remove all (the old) affiliations on the account
9430 for row in ac.get_account_types(filter_expired=False):
9431 ac.del_account_type(row['ou_id'], row['affiliation'])
9432
9433 # Automatic selection of affiliation. This could be used if the user
9434 # should not choose affiliations.
9435 # # Sort affiliations according to creation date (newest first), and
9436 # # try to save it for later. If there exists no affiliations, we'll
9437 # # raise an error, since we'll need an affiliation to copy from the
9438 # # person to the account.
9439 # try:
9440 # tmp = sorted(pe.get_affiliations(),
9441 # key=lambda i: i['create_date'], reverse=True)[0]
9442 # ou, aff = tmp['ou_id'], tmp['affiliation']
9443 # except IndexError:
9444 # raise CerebrumError('Person must have an affiliation')
9445
9446 # We set the affiliation selected by the operator.
9447 self._user_create_set_account_type(ac, ac.owner_id, aff_ou['ou_id'], \
9448 aff_ou['aff'])
9449
9450 # And promote posix
9451 old_uid = self._lookup_old_uid(ac.entity_id)
9452 if old_uid is None:
9453 uid = pu.get_free_uid()
9454 else:
9455 uid = old_uid
9456
9457 shell = self.const.posix_shell_bash
9458
9459 # Populate the posix user, and write it to the database
9460 pu.populate(uid, None, None, shell, parent=ac,
9461 creator_id=operator.get_entity_id())
9462 try:
9463 pu.write_db()
9464 except self.db.IntegrityError, e:
9465 self.logger.debug("IntegrityError: %s" % e)
9466 self.db.rollback()
9467 raise CerebrumError('Please contact brukerreg in order to restore')
9468
9469 # Unset the expire date
9470 ac.expire_date = None
9471
9472 # Add them spreads
9473 for s in cereconf.BOFHD_NEW_USER_SPREADS:
9474 if not ac.has_spread(self.const.Spread(s)):
9475 ac.add_spread(self.const.Spread(s))
9476
9477 # And remove them quarantines (except those defined in cereconf)
9478 for q in ac.get_entity_quarantine():
9479 if str(self.const.Quarantine(q['quarantine_type'])) not in \
9480 cereconf.BOFHD_RESTORE_USER_SAVE_QUARANTINES:
9481 ac.delete_entity_quarantine(q['quarantine_type'])
9482
9483 # We set the new homedir
9484 default_home_spread = self._get_constant(self.const.Spread,
9485 cereconf.DEFAULT_HOME_SPREAD,
9486 'spread')
9487
9488 homedir_id = pu.set_homedir(
9489 disk_id=disk_id, home=home,
9490 status=self.const.home_status_not_created)
9491 pu.set_home(default_home_spread, homedir_id)
9492
9493 # We'll set a new password and store it for printing
9494 passwd = ac.make_passwd(ac.account_name)
9495 ac.set_password(passwd)
9496
9497 operator.store_state('new_account_passwd',
9498 {'account_id': int(ac.entity_id),
9499 'password': passwd})
9500
9501 # We'll need to write to the db, in order to store stuff.
9502 try:
9503 ac.write_db()
9504 except self.db.IntegrityError, e:
9505 self.logger.debug("IntegrityError (ac.write_db): %s" % e)
9506 self.db.rollback()
9507 raise CerebrumError('Please contact brukerreg in order to restore')
9508
9509 # Return string with some info
9510 if ac.get_entity_quarantine():
9511 note = '\nNotice: Account is quarantined!'
9512 else:
9513 note = ''
9514
9515 if old_uid is None:
9516 tmp = ', new uid=%i' % uid
9517 else:
9518 tmp = ', reused old uid=%i' % old_uid
9519
9520 return '''OK, promoted %s to posix user%s.
9521Password altered. Use misc list_password to print or view the new password.%s'''\
9522 % (accountname, tmp, note)
9523
9524 # user set_disk_status
9525 all_commands['user_set_disk_status'] = Command(
9526 ('user', 'set_disk_status'), AccountName(),
9527 SimpleString(help_ref='string_disk_status'),
9528 perm_filter='can_create_disk')
9529 def user_set_disk_status(self, operator, accountname, status):
9530 try:
9531 status = self.const.AccountHomeStatus(status)
9532 int(status)
9533 except Errors.NotFoundError:
9534 raise CerebrumError, "Unknown status"
9535 account = self._get_account(accountname)
9536 # this is not exactly right, we should probably
9537 # implement a can_set_disk_status-function, but no
9538 # appropriate criteria is readily available for this
9539 # right now
9540 self.ba.can_create_disk(operator.get_entity_id(),query_run_any=True)
9541 ah = account.get_home(self.const.spread_uit_nis_user)
9542 account.set_homedir(current_id=ah['homedir_id'], status=status)
9543 return "OK, set home-status for %s to %s" % (accountname, status)
9544
9545 # user set_expire
9546 all_commands['user_set_expire'] = Command(
9547 ('user', 'set_expire'), AccountName(), Date(),
9548 perm_filter='can_delete_user')
9549 def user_set_expire(self, operator, accountname, date):
9550 if not self.ba.is_superuser(operator.get_entity_id()):
9551 raise PermissionDenied("Currently limited to superusers")
9552 account = self._get_account(accountname)
9553 # self.ba.can_delete_user(operator.get_entity_id(), account)
9554 account.expire_date = self._parse_date(date)
9555 account.write_db()
9556 return "OK, set expire-date for %s to %s" % (accountname, date)
9557
9558 # user set_np_type
9559 all_commands['user_set_np_type'] = Command(
9560 ('user', 'set_np_type'), AccountName(), SimpleString(help_ref="string_np_type"),
9561 perm_filter='can_delete_user')
9562 def user_set_np_type(self, operator, accountname, np_type):
9563 account = self._get_account(accountname)
9564 self.ba.can_delete_user(operator.get_entity_id(), account)
9565 account.np_type = self._get_constant(self.const.Account, np_type,
9566 "account type")
9567 account.write_db()
9568 return "OK, set np-type for %s to %s" % (accountname, np_type)
9569
9570 def user_set_owner_prompt_func(self, session, *args):
9571 all_args = list(args[:])
9572 if not all_args:
9573 return {'prompt': 'Account name'}
9574 account_name = all_args.pop(0)
9575 if not all_args:
9576 return {'prompt': 'Entity type (group/person)',
9577 'default': 'person'}
9578 entity_type = all_args.pop(0)
9579 if not all_args:
9580 return {'prompt': 'Id of the type specified above'}
9581 id = all_args.pop(0)
9582 if entity_type == 'person':
9583 if not all_args:
9584 person = self._get_person(*self._map_person_id(id))
9585 map = [(("%-8s %s", "Num", "Affiliation"), None)]
9586 for aff in person.get_affiliations():
9587 ou = self._get_ou(ou_id=aff['ou_id'])
9588 name = "%s@%s" % (
9589 self.const.PersonAffStatus(aff['status']),
9590 self._format_ou_name(ou))
9591 map.append((("%s", name),
9592 {'ou_id': int(aff['ou_id']), 'aff': int(aff['affiliation'])}))
9593 if not len(map) > 1:
9594 raise CerebrumError(
9595 "Person has no affiliations.")
9596 return {'prompt': "Choose affiliation from list", 'map': map,
9597 'last_arg': True}
9598 else:
9599 if not all_args:
9600 return {'prompt': "Enter np_type",
9601 'help_ref': 'string_np_type',
9602 'last_arg': True}
9603 np_type = all_args.pop(0)
9604 raise CerebrumError, "Client called prompt func with too many arguments"
9605
9606 all_commands['user_set_owner'] = Command(
9607 ("user", "set_owner"), prompt_func=user_set_owner_prompt_func,
9608 perm_filter='is_superuser')
9609 def user_set_owner(self, operator, *args):
9610 if args[1] == 'person':
9611 accountname, entity_type, id, affiliation = args
9612 new_owner = self._get_person(*self._map_person_id(id))
9613 else:
9614 accountname, entity_type, id, np_type = args
9615 new_owner = self._get_entity(entity_type, id)
9616 np_type = self._get_constant(self.const.Account, np_type,
9617 "account type")
9618
9619 account = self._get_account(accountname)
9620 if not self.ba.is_superuser(operator.get_entity_id()):
9621 raise PermissionDenied("only superusers may assign account ownership")
9622 new_owner = self._get_entity(entity_type, id)
9623 if account.owner_type == self.const.entity_person:
9624 for row in account.get_account_types(filter_expired=False):
9625 account.del_account_type(row['ou_id'], row['affiliation'])
9626 account.owner_type = new_owner.entity_type
9627 account.owner_id = new_owner.entity_id
9628 if args[1] == 'group':
9629 account.np_type = np_type
9630 account.write_db()
9631 if new_owner.entity_type == self.const.entity_person:
9632 ou_id, affiliation = affiliation['ou_id'], affiliation['aff']
9633 self._user_create_set_account_type(account, account.owner_id,
9634 ou_id, affiliation)
9635 return "OK, set owner of %s to %s" % (
9636 accountname, self._get_name_from_object(new_owner))
9637
9638 # user shell
9639 all_commands['user_shell'] = Command(
9640 ("user", "shell"), AccountName(), PosixShell(default="bash"))
9641 def user_shell(self, operator, accountname, shell=None):
9642 account = self._get_account(accountname, actype="PosixUser")
9643 shell = self._get_shell(shell)
9644 self.ba.can_set_shell(operator.get_entity_id(), account, shell)
9645 account.shell = shell
9646 account.write_db()
9647 return "OK, set shell for %s to %s" % (accountname, shell)
9648
9649 #
9650 # commands that are noe available in jbofh, but used by other clients
9651 #
9652
9653 all_commands['get_persdata'] = None
9654
9655 def get_persdata(self, operator, uname):
9656 if not self.ba.is_postmaster(operator.get_entity_id()):
9657 raise PermissionDenied("Currently limited to superusers")
9658 ac = self._get_account(uname)
9659 person_id = "entity_id:%i" % ac.owner_id
9660 person = self._get_person(*self._map_person_id(person_id))
9661 ret = {
9662 'is_personal': len(ac.get_account_types()),
9663 'fnr': [{'id': r['external_id'],
9664 'source':
9665 str(self.const.AuthoritativeSystem(r['source_system']))}
9666 for r in person.get_external_id(id_type=self.const.externalid_fodselsnr)]
9667 }
9668 ac_types = ac.get_account_types(all_persons_types=True)
9669 if ret['is_personal']:
9670 ac_types.sort(lambda x,y: int(x['priority']-y['priority']))
9671 for at in ac_types:
9672 ac2 = self._get_account(at['account_id'], idtype='id')
9673 ret.setdefault('users', []).append(
9674 (ac2.account_name, '%s@ulrik.uit.no' % ac2.account_name,
9675 at['priority'], at['ou_id'],
9676 str(self.const.PersonAffiliation(at['affiliation']))))
9677 # TODO: kall ac.list_accounts_by_owner_id(ac.owner_id) for
9678 # MÃ¥ hente ikke-personlige konti?
9679 ret['home'] = ac.resolve_homedir(disk_id=ac.disk_id, home=ac.home)
9680 ret['navn'] = {'cached': person.get_name(
9681 self.const.system_cached, self.const.name_full)}
9682 for key, variant in (("work_title", self.const.work_title),
9683 ("personal_title", self.const.personal_title)):
9684 try:
9685 ret[key] = person.get_name_with_language(
9686 name_variant=variant,
9687 name_language=self.const.language_nb)
9688 except (Errors.NotFoundError, Errors.TooManyRowsError):
9689 pass
9690 return ret
9691
9692 #
9693 # misc helper functions.
9694 # TODO: These should be protected so that they are not remotely callable
9695 #
9696
9697 def _get_account(self, id, idtype=None, actype="Account"):
9698 if actype == 'Account':
9699 account = self.Account_class(self.db)
9700 elif actype == 'PosixUser':
9701 account = Utils.Factory.get('PosixUser')(self.db)
9702 account.clear()
9703 try:
9704 if idtype is None:
9705 if id.find(":") != -1:
9706 idtype, id = id.split(":", 1)
9707 if len(id) == 0:
9708 raise CerebrumError, "Must specify id"
9709 else:
9710 idtype = 'name'
9711 if idtype == 'name':
9712 account.find_by_name(id, self.const.account_namespace)
9713 elif idtype == 'id':
9714 if isinstance(id, str) and not id.isdigit():
9715 raise CerebrumError, "Entity id must be a number"
9716 account.find(id)
9717 elif idtype == 'uid':
9718 if isinstance(id, str) and not id.isdigit():
9719 raise CerebrumError, 'uid must be a number'
9720 if actype != 'PosixUser':
9721 account = Utils.Factory.get('PosixUser')(self.db)
9722 account.clear()
9723 account.find_by_uid(id)
9724 else:
9725 raise CerebrumError, "unknown idtype: '%s'" % idtype
9726 except Errors.NotFoundError:
9727 raise CerebrumError, "Could not find %s with %s=%s" % (actype, idtype, id)
9728 return account
9729
9730 def _get_email_domain(self, name):
9731 ed = Email.EmailDomain(self.db)
9732 try:
9733 ed.find_by_domain(name)
9734 except Errors.NotFoundError:
9735 raise CerebrumError, "Unknown e-mail domain (%s)" % name
9736 return ed
9737
9738 def _get_email_server(self, name):
9739 es = Email.EmailServer(self.db)
9740 try:
9741 if isinstance(name, (int, long)):
9742 es.find(name)
9743 else:
9744 es.find_by_name(name)
9745 return es
9746 except Errors.NotFoundError:
9747 raise CerebrumError, "Unknown mail server: %s" % name
9748
9749 def _get_host(self, name):
9750 host = Utils.Factory.get('Host')(self.db)
9751 try:
9752 if isinstance(name, (int, long)):
9753 host.find(name)
9754 else:
9755 host.find_by_name(name)
9756 return host
9757 except Errors.NotFoundError:
9758 raise CerebrumError, "Unknown host: %s" % name
9759
9760 def _get_shell(self, shell):
9761 return self._get_constant(self.const.PosixShell, shell, "shell")
9762
9763 def _get_opset(self, opset):
9764 aos = BofhdAuthOpSet(self.db)
9765 try:
9766 aos.find_by_name(opset)
9767 except Errors.NotFoundError:
9768 raise CerebrumError, "Could not find op set with name %s" % opset
9769 return aos
9770
9771 def _format_ou_name(self, ou):
9772 short_name = ou.get_name_with_language(
9773 name_variant=self.const.ou_name_short,
9774 name_language=self.const.language_nb,
9775 default="")
9776 # return None if ou does not have stedkode
9777 if ou.fakultet != None:
9778 return "%02i%02i%02i (%s)" % (ou.fakultet, ou.institutt, ou.avdeling,short_name)
9779 else:
9780 return "None"
9781
9782 def _get_group_opcode(self, operator):
9783 if operator is None:
9784 return self.const.group_memberop_union
9785 if operator == 'union':
9786 return self.const.group_memberop_union
9787 if operator == 'intersection':
9788 return self.const.group_memberop_intersection
9789 if operator == 'difference':
9790 return self.const.group_memberop_difference
9791 raise CerebrumError("unknown group opcode: '%s'" % operator)
9792
9793 def _get_entity(self, idtype=None, ident=None):
9794 if ident is None:
9795 raise CerebrumError("Invalid id")
9796 if idtype == 'account':
9797 return self._get_account(ident)
9798 if idtype == 'person':
9799 return self._get_person(*self._map_person_id(ident))
9800 if idtype == 'group':
9801 return self._get_group(ident)
9802 if idtype == 'stedkode':
9803 return self._get_ou(stedkode=ident)
9804 if idtype == 'host':
9805 return self._get_host(ident)
9806 if idtype is None:
9807 try:
9808 int(ident)
9809 except ValueError:
9810 raise CerebrumError("Expected int as id")
9811 ety = Entity.Entity(self.db)
9812 return ety.get_subclassed_object(ident)
9813 raise CerebrumError("Invalid idtype")
9814
9815 def _find_persons(self, arg):
9816 if arg.isdigit() and len(arg) > 10: # finn personer fra fnr
9817 arg = 'fnr:%s' % arg
9818 ret = []
9819 person = Utils.Factory.get('Person')(self.db)
9820 person.clear()
9821 if arg.find(":") != -1:
9822 idtype, value = arg.split(":", 1)
9823 if not value:
9824 raise CerebrumError, "Unable to parse person id %r" % arg
9825 if idtype == 'exp':
9826 if not value.isdigit():
9827 raise CerebrumError, "Export id must be a number"
9828 person.clear()
9829 try:
9830 person.find_by_export_id(value)
9831 ret.append({'person_id': person.entity_id})
9832 except Errors.NotFoundError:
9833 raise CerebrumError, "Unkown person id %r" % arg
9834 elif idtype == 'entity_id':
9835 if not value.isdigit():
9836 raise CerebrumError, "Entity id must be a number"
9837 person.clear()
9838 try:
9839 person.find(value)
9840 ret.append({'person_id': person.entity_id})
9841 except Errors.NotFoundError:
9842 raise CerebrumError, "Unkown person id %r" % arg
9843 elif idtype == 'fnr':
9844 for ss in cereconf.SYSTEM_LOOKUP_ORDER:
9845 try:
9846 person.clear()
9847 person.find_by_external_id(
9848 self.const.externalid_fodselsnr, value,
9849 source_system=getattr(self.const, ss))
9850 ret.append({'person_id': person.entity_id})
9851 except Errors.NotFoundError:
9852 pass
9853 elif arg.find("-") != -1:
9854 ret = person.find_persons_by_bdate(self._parse_date(arg))
9855
9856 else:
9857 raise CerebrumError, "Unable to parse person id %r" % arg
9858 return ret
9859
9860 def _get_entity_name(self, entity_id, entity_type=None):
9861 """Fetch a human-friendly name for the specified entity.
9862
9863 Overridden to return names only used at UiO.
9864
9865 @type entity_id: int
9866 @param entity_id:
9867 entity_id we are looking for.
9868
9869 @type entity_type: const.EntityType instance (or None)
9870 @param entity_type:
9871 Restrict the search to the specifide entity. This parameter is
9872 really a speed-up only -- entity_id in Cerebrum uniquely determines
9873 the entity_type. However, should we know it, we save 1 db lookup.
9874
9875 @rtype: str
9876 @return:
9877 Entity's name, obviously :) If none is found a magic string
9878 'notfound:<entity id>' is returned (it's not perfect, but it's better
9879 than nothing at all).
9880
9881 """
9882 if entity_type == self.const.entity_ou:
9883 ou = self._get_ou(ou_id=entity_id)
9884 return self._format_ou_name(ou)
9885 # Use default values for types like account and group:
9886 return super(BofhdExtension, self)._get_entity_name(entity_id=entity_id,
9887 entity_type=entity_type)
9888
9889 def _get_disk(self, path, host_id=None, raise_not_found=True):
9890 disk = Utils.Factory.get('Disk')(self.db)
9891 try:
9892 if isinstance(path, str):
9893 disk.find_by_path(path, host_id)
9894 else:
9895 disk.find(path)
9896 return disk, disk.entity_id, None
9897 except Errors.NotFoundError:
9898 if raise_not_found:
9899 raise CerebrumError("Unknown disk: %s" % path)
9900 return disk, None, path
9901
9902 def _is_yes(self, val):
9903 if isinstance(val, str) and val.lower() in ('y', 'yes', 'ja', 'j'):
9904 return True
9905 return False
9906
9907 # The next two functions require all affiliations to be in upper case,
9908 # and all affiliation statuses to be in lower case. If this changes,
9909 # the user will have to type exact case.
9910 def _get_affiliationid(self, code_str):
9911 try:
9912 c = self.const.PersonAffiliation(code_str.upper())
9913 # force a database lookup to see if it's a valid code
9914 int(c)
9915 return c
9916 except Errors.NotFoundError:
9917 raise CerebrumError("Unknown affiliation")
9918
9919 def _get_affiliation_statusid(self, affiliation, code_str):
9920 try:
9921 c = self.const.PersonAffStatus(affiliation, code_str.lower())
9922 int(c)
9923 return c
9924 except Errors.NotFoundError:
9925 raise CerebrumError("Unknown affiliation status")
9926
9927 def _get_constant(self, code_cls, code_str, code_type="value"):
9928 c = code_cls(code_str)
9929 try:
9930 int(c)
9931 except Errors.NotFoundError:
9932 raise CerebrumError("Unknown %s: %s" % (code_type, code_str))
9933 return c
9934
9935
9936 hidden_commands['get_constant_description'] = Command(
9937 ("misc", "get_constant_description"),
9938 SimpleString(), # constant class
9939 SimpleString(optional=True),
9940 fs=FormatSuggestion("%-15s %s",
9941 ("code_str", "description")))
9942 def get_constant_description(self, operator, code_cls, code_str=None):
9943 """Fetch constant descriptions.
9944
9945 There are no permissions checks for this method -- it can be called by
9946 anyone without any restrictions.
9947
9948 @type code_cls: basestring
9949 @param code_cls:
9950 Class (name) for the constants to fetch.
9951
9952 @type code_str: basestring or None
9953 @param code_str:
9954 code_str for the specific constant to fetch. If None is specified,
9955 *all* constants of the given type are retrieved.
9956
9957 @rtype: dict or a sequence of dicts
9958 @return:
9959 Description of the specified constants. Each dict has 'code' and
9960 'description' keys.
9961 """
9962
9963 if not hasattr(self.const, code_cls):
9964 raise CerebrumError("%s is not a constant type" % code_cls)
9965
9966 kls = getattr(self.const, code_cls)
9967 if not issubclass(kls, self.const.CerebrumCode):
9968 raise CerebrumError("%s is not a valid constant class" % code_cls)
9969
9970 if code_str is not None:
9971 c = self._get_constant(kls, code_str)
9972 return {"code": int(c),
9973 "code_str": str(c),
9974 "description": c.description}
9975
9976 # Fetch all of the constants of the specified type
9977 return [{"code": int(x),
9978 "code_str": str(x),
9979 "description": x.description}
9980 for x in self.const.fetch_constants(kls)]
9981 # end get_constant_description
9982
9983
9984 def _parse_date_from_to(self, date):
9985 date_start = self._today()
9986 date_end = None
9987 if date:
9988 tmp = date.split("--")
9989 if len(tmp) == 2:
9990 if tmp[0]: # string could start with '--'
9991 date_start = self._parse_date(tmp[0])
9992 date_end = self._parse_date(tmp[1])
9993 elif len(tmp) == 1:
9994 date_end = self._parse_date(date)
9995 else:
9996 raise CerebrumError, "Incorrect date specification: %s." % date
9997 return (date_start, date_end)
9998
9999 def _parse_date(self, date):
10000 """Convert a written date into DateTime object. Possible
10001 syntaxes are:
10002
10003 YYYY-MM-DD (2005-04-03)
10004 YYYY-MM-DDTHH:MM (2005-04-03T02:01)
10005 THH:MM (T02:01)
10006
10007 Time of day defaults to midnight. If date is unspecified, the
10008 resulting time is between now and 24 hour into future.
10009
10010 """
10011 if not date:
10012 # TBD: Is this correct behaviour? mx.DateTime.DateTime
10013 # objects allow comparison to None, although that is
10014 # hardly what we expect/want.
10015 return None
10016 if isinstance(date, DateTime.DateTimeType):
10017 # Why not just return date? Answer: We do some sanity
10018 # checks below.
10019 date = date.Format("%Y-%m-%dT%H:%M")
10020 if date.count('T') == 1:
10021 date, time = date.split('T')
10022 try:
10023 hour, min = [int(x) for x in time.split(':')]
10024 except ValueError:
10025 raise CerebrumError, "Time of day must be on format HH:MM"
10026 if date == '':
10027 now = DateTime.now()
10028 target = DateTime.Date(now.year, now.month, now.day, hour, min)
10029 if target < now:
10030 target += DateTime.DateTimeDelta(1)
10031 date = target.Format("%Y-%m-%d")
10032 else:
10033 hour = min = 0
10034 try:
10035 y, m, d = [int(x) for x in date.split('-')]
10036 except ValueError:
10037 raise CerebrumError, "Dates must be on format YYYY-MM-DD"
10038 # TODO: this should be a proper delta, but rather than using
10039 # pgSQL specific code, wait until Python has standardised on a
10040 # Date-type.
10041 if y > 2050:
10042 raise CerebrumError, "Too far into the future: %s" % date
10043 if y < 1800:
10044 raise CerebrumError, "Too long ago: %s" % date
10045 try:
10046 return DateTime.Date(y, m, d, hour, min)
10047 except:
10048 raise CerebrumError, "Illegal date: %s" % date
10049
10050 def _today(self):
10051 return self._parse_date("%d-%d-%d" % time.localtime()[:3])
10052
10053 def _format_from_cl(self, format, val):
10054 if val is None:
10055 return ''
10056
10057 if format == 'affiliation':
10058 return str(self.const.PersonAffiliation(val))
10059 elif format == 'disk':
10060 disk = Utils.Factory.get('Disk')(self.db)
10061 try:
10062 disk.find(val)
10063 return disk.path
10064 except Errors.NotFoundError:
10065 return "deleted_disk:%s" % val
10066 elif format == 'date':
10067 return val
10068 elif format == 'timestamp':
10069 return str(val)
10070 elif format == 'entity':
10071 return self._get_entity_name(int(val))
10072 elif format == 'extid':
10073 return str(self.const.EntityExternalId(val))
10074 elif format == 'homedir':
10075 return 'homedir_id:%s' % val
10076 elif format == 'id_type':
10077 return str(self.const.ChangeType(val))
10078 elif format == 'home_status':
10079 return str(self.const.AccountHomeStatus(val))
10080 elif format == 'int':
10081 return str(val)
10082 elif format == 'name_variant':
10083 # Name variants are stored in two separate code-tables; if
10084 # one doesn't work, try the other
10085 try:
10086 name_variant = str(self.const.PersonName(val))
10087 return name_variant
10088 except:
10089 return str(self.const.EntityNameCode(val))
10090 elif format == 'ou':
10091 ou = self._get_ou(ou_id=val)
10092 return self._format_ou_name(ou)
10093 elif format == 'quarantine_type':
10094 return str(self.const.Quarantine(val))
10095 elif format == 'source_system':
10096 return str(self.const.AuthoritativeSystem(val))
10097 elif format == 'spread_code':
10098 return str(self.const.Spread(val))
10099 elif format == 'string':
10100 return str(val)
10101 elif format == 'trait':
10102 try:
10103 return str(self.const.EntityTrait(val))
10104 except Errors.NotFoundError:
10105 # Trait has been deleted from the DB, so we can't know which it was
10106 return "<unknown>"
10107 elif format == 'value_domain':
10108 return str(self.const.ValueDomain(val))
10109 elif format == 'rolle_type':
10110 try:
10111 val = int(val)
10112 except ValueError:
10113 pass
10114 return str(self.const.EphorteRole(val))
10115 elif format == 'perm_type':
10116 return str(self.const.EphortePermission(val))
10117 elif format == 'bool':
10118 if val == 'T':
10119 return str(True)
10120 elif val == 'F':
10121 return str(False)
10122 else:
10123 return str(bool(val))
10124 else:
10125 self.logger.warn("bad cl format: %s", repr((format, val)))
10126 return ''
10127
10128 def _format_changelog_entry(self, row):
10129 dest = row['dest_entity']
10130 if dest is not None:
10131 try:
10132 dest = self._get_entity_name(dest)
10133 except Errors.NotFoundError:
10134 dest = repr(dest)
10135 this_cl_const = self.const.ChangeType(row['change_type_id'])
10136 msg = this_cl_const.msg_string % {
10137 'subject': self._get_entity_name(row['subject_entity']),
10138 'dest': dest}
10139
10140 # Append information from change_params to the string. See
10141 # _ChangeTypeCode.__doc__
10142 if row['change_params']:
10143 try:
10144 params = pickle.loads(row['change_params'])
10145 except TypeError:
10146 self.logger.error("Bogus change_param in change_id=%s, row: %s",
10147 row['change_id'], row)
10148 raise
10149 else:
10150 params = {}
10151
10152 if this_cl_const.format:
10153 for f in this_cl_const.format:
10154 repl = {}
10155 for part in re.findall(r'%\([^\)]+\)s', f):
10156 fmt_type, key = part[2:-2].split(':')
10157 try:
10158 repl['%%(%s:%s)s' % (fmt_type, key)] = self._format_from_cl(
10159 fmt_type, params.get(key, None))
10160 except Exception, e:
10161 self.logger.warn("Failed applying %s to %s for change-id: %d" % (
10162 part, repr(params.get(key)), row['change_id']), exc_info=1)
10163 if [x for x in repl.values() if x]:
10164 for k, v in repl.items():
10165 f = f.replace(k, v)
10166 msg += ", " + f
10167 by = row['change_program'] or self._get_entity_name(row['change_by'])
10168 return {'timestamp': row['tstamp'],
10169 'change_by': by,
10170 'message': msg}
10171
10172 def _convert_ticks_to_timestamp(self, ticks):
10173 if ticks is None:
10174 return None
10175 return DateTime.DateTimeFromTicks(ticks)
10176
10177 def _lookup_old_uid(self, account_id):
10178 uid = None
10179 for r in self.db.get_log_events(
10180 0, subject_entity=account_id, types=[self.const.posix_demote]):
10181 uid = pickle.loads(r['change_params'])['uid']
10182 return uid
10183
10184 def _date_human_readable(self, date):
10185 "Convert date to something human-readable."
10186
10187 if hasattr(date, "strftime"):
10188 return date.strftime("%Y-%m-%dT%H:%M:%S")
10189
10190 return str(date)