Guest User

Untitled

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