Guest User

Untitled

a guest
Nov 5th, 2018
434
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 450.33 KB | None | 0 0
  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.  
  21. import time
  22. import re
  23. import imaplib
  24. import ssl
  25. import pickle
  26. import socket
  27.  
  28. from mx import DateTime
  29. from flanker.addresslib import address as email_validator
  30.  
  31. import cereconf
  32. from Cerebrum import Database
  33. from Cerebrum import Entity
  34. from Cerebrum import Errors
  35. from Cerebrum import Metainfo
  36. from Cerebrum.Constants import _LanguageCode
  37. from Cerebrum import Utils
  38. from Cerebrum.modules import Email
  39. from Cerebrum.modules.pwcheck.checker import (check_password,
  40. PasswordNotGoodEnough,
  41. RigidPasswordNotGoodEnough,
  42. PhrasePasswordNotGoodEnough)
  43. from Cerebrum.modules.pwcheck.history import PasswordHistory
  44. from Cerebrum.modules.bofhd.bofhd_core import BofhdCommonMethods
  45. from Cerebrum.modules.bofhd.cmd_param import *
  46. from Cerebrum.modules.bofhd.errors import CerebrumError, PermissionDenied
  47. from Cerebrum.modules.bofhd.utils import BofhdRequests
  48. from Cerebrum.modules.bofhd.auth import (BofhdAuthOpSet,
  49. AuthConstants,
  50. BofhdAuthOpTarget,
  51. BofhdAuthRole)
  52. from Cerebrum.modules.bofhd.help import Help
  53. from Cerebrum.modules.no import fodselsnr
  54. from Cerebrum.modules.bofhd import bofhd_core_help
  55. from Cerebrum.modules.no.uit.bofhd_auth import BofhdAuth
  56. from Cerebrum.modules.no.uit.access_FS import FS
  57. from Cerebrum.modules.no.uit.DiskQuota import DiskQuota
  58. from 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.
  63. def format_day(field):
  64. fmt = "yyyy-MM-dd" # 10 characters wide
  65. return ":".join((field, "date", fmt))
  66.  
  67.  
  68. def format_time(field):
  69. fmt = "yyyy-MM-dd HH:mm" # 16 characters wide
  70. return ':'.join((field, "date", fmt))
  71.  
  72.  
  73. def 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.  
  88. class TimeoutException(Exception):
  89. pass
  90.  
  91.  
  92. class ConnectException(Exception):
  93. pass
  94.  
  95.  
  96. class RTQueue(Parameter):
  97. _type = 'rtQueue'
  98. _help_ref = 'rt_queue'
  99.  
  100.  
  101. # TODO: move more UiO cruft from bofhd/auth.py in here
  102. class 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.  
  130. class 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="""
  2960. This is an automatically generated e-mail.
  2961.  
  2962. The following e-mail list addresses have just been removed from Cerebrum. Keep
  2963. this message, in case a restore is requested later.
  2964.  
  2965. Addresses 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.
  9521. Password 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)
Add Comment
Please, Sign In to add comment