示例#1
0
class BofhdOUDiskMappingCommands(BofhdCommandBase):
    """OU Disk Mapping commands."""

    all_commands = {}
    authz = BofhdOUDiskMappingAuth

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        return merge_help_strings((HELP_GROUP, HELP_CMD, HELP_ARGS),
                                  get_help_strings())

    def __find_ou(self, ou):
        # Try to find the OU the user wants to edit
        ou_class = self.OU_class(self.db)
        if ou.startswith("id:"):
            ou = ou[3:]
            # Assume we got the entity id of the ou
            try:
                ou_class.find(ou)
            except Errors.NotFoundError:
                raise CerebrumError("Unknown OU id {}".format(
                    six.text_type(ou)))
        elif len(ou) == 6:
            # Assume we got a stedkode
            fakultet = ou[:2]
            institutt = ou[2:4]
            avdeling = ou[4:]
            institusjon = cereconf.DEFAULT_INSTITUSJONSNR
            try:
                ou_class.find_stedkode(fakultet, institutt, avdeling,
                                       institusjon)
            except Errors.NotFoundError:
                raise CerebrumError("Unknown OU with stedkode {}".format(
                    six.text_type(ou)))
        else:
            raise CerebrumError(
                "Unable to parse OU id or stedkode {}".format(ou))
        return ou_class

    #
    # ou homedir_set <path> <ou> <aff> <status>
    #
    all_commands["ou_homedir_add"] = Command(
        ("ou", "homedir_add"),
        SimpleString(help_ref="path"),
        SimpleString(help_ref="ou"),
        SimpleString(help_ref="aff", optional=True),
        fs=FormatSuggestion(
            "Set homedir='%s' for affiliation %s at OU %i %s",
            ("path", "aff", "ou", "stedkode"),
        ),
        perm_filter="can_add_ou_path",
    )

    def ou_homedir_add(self, operator, disk, ou, aff=None):
        """Set default home dir for aff at OU

        :param operator:
        :param str ou:
        :param str or None aff:
        :param str disk:
        :rtype: dict
        :return: client output
        """
        ou_class = self.__find_ou(ou)
        # Check if allowed to set default home dir for this Aff at this OU
        if not self.ba.can_add_ou_path(operator.get_entity_id(),
                                       ou_class.entity_id):
            raise NO_ACCESS_ERROR

        # Try to find the Affiliation the user wants to edit
        aff_str = "*"
        status = None
        if aff:
            try:
                aff, status = self.const.get_affiliation(aff)
            except Errors.NotFoundError:
                raise CerebrumError('Unknown affiliation {}'.format(aff))
            if status:
                aff_str = six.text_type(status)
            else:
                aff_str = six.text_type(aff)

        # Try to find the disk the users want to set
        disk_class = Factory.get("Disk")(self.db)
        if disk.startswith("id:") or disk.isdigit():
            # Assume this is an entity id
            try:
                disk_class.find(disk)
            except Errors.NotFoundError:
                raise CerebrumError("Unknown disk with id {}".format(
                    six.text_type(disk)))
        else:
            # Assume the user wrote a path
            try:
                disk_class.find_by_path(disk)
            except Errors.NotFoundError:
                raise CerebrumError("Unknown disk with path {}".format(
                    six.text_type(disk)))

        # Set the path and return some information to the user
        ous = OUDiskMapping(self.db)
        ous.add(ou_class.entity_id, aff, status, disk_class.entity_id)
        return {
            "ou":
            ou_class.entity_id,
            "aff":
            aff_str,
            "path":
            six.text_type(disk_class.path),
            "stedkode":
            "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr(
                ou_class, 'get_stedkode') else ""
        }

    #
    # ou homedir_clear <ou> <aff> <status>
    #
    all_commands["ou_homedir_remove"] = Command(
        ("ou", "homedir_remove"),
        SimpleString(help_ref="ou"),
        SimpleString(help_ref="aff", optional=True),
        fs=FormatSuggestion(
            "Removed homedir for affiliation %s at OU %i %s",
            ("aff", "ou", "stedkode"),
        ),
        perm_filter="can_remove_ou_path",
    )

    def ou_homedir_remove(self, operator, ou, aff=None):
        """Remove default home dir for aff at OU

        If you want to remove the homedir for just the OU and any aff and
        status, use:
        > ou homedir_remove ou

        For OU with affiliation and wildcard status use:
        > ou homedir_remove ou aff

        For OU with affiliation and status use:
        > ou homedir_remove ou aff/status

        :param operator:
        :param str ou:
        :param str or None aff:
        :rtype: dict
        :return: client output
        """
        # Try to find the OU the user wants to edit
        ou_class = self.__find_ou(ou)

        # Check if allowed to clear home dir for this OU
        if not self.ba.can_remove_ou_path(operator.get_entity_id(),
                                          ou_class.entity_id):
            raise NO_ACCESS_ERROR

        # Try to find the Affiliation the user wants to edit
        aff_str = "*"
        status = None
        if aff:
            try:
                aff, status = self.const.get_affiliation(aff)
            except Errors.NotFoundError:
                raise CerebrumError('Unknown affiliation {}'.format(aff))
            if status:
                aff_str = six.text_type(status)
            else:
                aff_str = six.text_type(aff)

        # Clear the path and return some information to the user
        ous = OUDiskMapping(self.db)
        ous.delete(ou_class.entity_id, aff, status)
        return {
            "ou":
            ou_class.entity_id,
            "aff":
            aff_str,
            "stedkode":
            "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr(
                ou_class, 'get_stedkode') else ""
        }

    #
    # ou homedir_get <ou> <aff> <status>
    #
    all_commands["ou_homedir_list"] = Command(
        ("ou", "homedir_list"),
        SimpleString(help_ref="ou"),
        SimpleString(help_ref="aff", optional=True),
        fs=FormatSuggestion(
            "%8s %12i %26s %s",
            ("stedkode", "ou", "aff", "disk"),
            hdr="%8s %12s %26s %s" % ("Stedkode", "OU", "Affiliation", "Disk"),
        ),
        perm_filter="can_list_ou_path",
    )

    def ou_homedir_list(self, operator, ou, aff=None):
        """Get default home dir for aff at OU

        :param operator:
        :param str ou:
        :param str or None aff:
        :rtype: dict
        :return: client output
        """
        if aff is None:
            aff = NotSet

        # Try to find the OU the user wants to edit
        ou_class = self.__find_ou(ou)

        # Check if allowed to clear home dir for this OU
        if not self.ba.can_list_ou_path(operator.get_entity_id(),
                                        ou_class.entity_id):
            raise NO_ACCESS_ERROR

        # Try to find the Affiliation the user wants to edit
        if aff:
            try:
                aff, status = self.const.get_affiliation(aff)
            except Errors.NotFoundError:
                raise CerebrumError('Unknown affiliation {}'.format(aff))
            if status:
                aff = status
        # Get the path and return some information to the user
        ous = OUDiskMapping(self.db)
        disk_class = Factory.get("Disk")(self.db)

        ret = []
        for row in ous.search(ou_class.entity_id, aff, any_status=True):
            if row["status_code"] is not None:
                aff_str = str(self.const.PersonAffStatus(row["status_code"]))
            elif row["aff_code"] is not None:
                aff_str = str(self.const.PersonAffiliation(row["aff_code"]))
            else:
                aff_str = None

            ou_class.clear()
            ou_class.find(row["ou_id"])
            # Get the stedkode of the OU if the stedkode module is present
            stedkode = (ou_class.get_stedkode() if hasattr(
                ou_class, "get_stedkode") else "")
            disk_class.clear()
            disk_class.find(row["disk_id"])
            ret.append({
                "stedkode": stedkode,
                "ou": row["ou_id"],
                "aff": aff_str,
                "disk": disk_class.path,
            })
        return ret
class BofhdExtension(BofhdCommandBase):

    all_commands = {}
    hidden_commands = {}
    authz = RequestsAuth

    #
    # misc change_request <request-id> <datetime>
    #
    all_commands['misc_change_request'] = Command(("misc", "change_request"),
                                                  Id(help_ref="id:request_id"),
                                                  DateTimeString())

    def misc_change_request(self, operator, request_id, datetime):
        if not request_id:
            raise CerebrumError('Request id required')
        if not datetime:
            raise CerebrumError('Date required')
        datetime = self._parse_date(datetime)
        br = BofhdRequests(self.db, self.const)
        old_req = br.get_requests(request_id=request_id)
        if not old_req:
            raise CerebrumError("There is no request with id=%r" % request_id)
        else:
            # If there is anything, it's at most one
            old_req = old_req[0]
        # If you are allowed to cancel a request, you can change it :)
        self.ba.can_cancel_request(operator.get_entity_id(), request_id)
        br.delete_request(request_id=request_id)
        br.add_request(operator.get_entity_id(), datetime,
                       old_req['operation'], old_req['entity_id'],
                       old_req['destination_id'], old_req['state_data'])
        return "OK, altered request %s" % request_id

    #
    # misc list_bofhd_request_types
    #
    all_commands['misc_list_bofhd_request_types'] = Command(
        ("misc", "list_bofhd_request_types"),
        fs=FormatSuggestion("%-20s %s", ("code_str", "description"),
                            hdr="%-20s %s" % ("Code", "Description")))

    def misc_list_bofhd_request_types(self, operator):
        br = BofhdRequests(self.db, self.const)
        result = []
        for row in br.get_operations():
            br_code = self.const.BofhdRequestOp(row['code_str'])
            result.append({
                'code_str': six.text_type(br_code).lstrip('br_'),
                'description': br_code.description,
            })
        return result

    #
    # misc cancel_request
    #
    all_commands['misc_cancel_request'] = Command(
        ("misc", "cancel_request"), SimpleString(help_ref='id:request_id'))

    def misc_cancel_request(self, operator, req):
        if req.isdigit():
            req_id = int(req)
        else:
            raise CerebrumError("Request-ID must be a number")
        br = BofhdRequests(self.db, self.const)
        if not br.get_requests(request_id=req_id):
            raise CerebrumError("Request ID %d not found" % req_id)
        self.ba.can_cancel_request(operator.get_entity_id(), req_id)
        br.delete_request(request_id=req_id)
        return "OK, %d canceled" % req_id

    #
    # misc list_requests
    #
    all_commands['misc_list_requests'] = Command(
        ("misc", "list_requests"),
        SimpleString(help_ref='string_bofh_request_search_by',
                     default='requestee'),
        SimpleString(help_ref='string_bofh_request_target', default='<me>'),
        fs=FormatSuggestion("%-7i %-10s %-16s %-16s %-10s %-20s %s",
                            ("id", "requestee", format_time("when"), "op",
                             "entity", "destination", "args"),
                            hdr="%-7s %-10s %-16s %-16s %-10s %-20s %s" %
                            ("Id", "Requestee", "When", "Op", "Entity",
                             "Destination", "Arguments")))

    def misc_list_requests(self, operator, search_by, destination):
        br = BofhdRequests(self.db, self.const)
        ret = []

        if destination == '<me>':
            destination = self._get_account(operator.get_entity_id(),
                                            idtype='id')
            destination = destination.account_name
        if search_by == 'requestee':
            account = self._get_account(destination)
            rows = br.get_requests(operator_id=account.entity_id, given=True)
        elif search_by == 'operation':
            try:
                destination = int(
                    self.const.BofhdRequestOp('br_' + destination))
            except Errors.NotFoundError:
                raise CerebrumError("Unknown request operation %s" %
                                    destination)
            rows = br.get_requests(operation=destination)
        elif search_by == 'disk':
            disk_id = self._get_disk(destination)[1]
            rows = br.get_requests(destination_id=disk_id)
        elif search_by == 'account':
            account = self._get_account(destination)
            rows = br.get_requests(entity_id=account.entity_id)
        else:
            raise CerebrumError("Unknown search_by criteria")

        for r in rows:
            op = self.const.BofhdRequestOp(r['operation'])
            dest = None
            ent_name = None
            if op in (self.const.bofh_move_user, self.const.bofh_move_request,
                      self.const.bofh_move_user_now):
                disk = self._get_disk(r['destination_id'])[0]
                dest = disk.path
            elif op in (self.const.bofh_move_give, ):
                dest = self._get_entity_name(r['destination_id'],
                                             self.const.entity_group)
            elif op in (self.const.bofh_email_create,
                        self.const.bofh_email_delete):
                dest = self._get_entity_name(r['destination_id'],
                                             self.const.entity_host)
            elif op in (self.const.bofh_sympa_create,
                        self.const.bofh_sympa_remove):
                ea = Email.EmailAddress(self.db)
                if r['destination_id'] is not None:
                    ea.find(r['destination_id'])
                    dest = ea.get_address()
                ea.clear()
                try:
                    ea.find(r['entity_id'])
                except Errors.NotFoundError:
                    ent_name = "<not found>"
                else:
                    ent_name = ea.get_address()
            if ent_name is None:
                ent_name = self._get_entity_name(r['entity_id'],
                                                 self.const.entity_account)
            if r['requestee_id'] is None:
                requestee = ''
            else:
                requestee = self._get_entity_name(r['requestee_id'],
                                                  self.const.entity_account)
            ret.append({
                'when': r['run_at'],
                'requestee': requestee,
                'op': six.text_type(op),
                'entity': ent_name,
                'destination': dest,
                'args': r['state_data'],
                'id': r['request_id']
            })
        ret.sort(lambda a, b: cmp(a['id'], b['id']))
        return ret
示例#3
0
class BofhdVoipCommands(BofhdCommonMethods):
    """Bofhd extension with voip commands."""

    all_commands = dict()
    parent_commands = False
    authz = bofhd_voip_auth.BofhdVoipAuth

    @classmethod
    def get_help_strings(cls):
        return (bofhd_voip_help.group_help, bofhd_voip_help.command_help,
                bofhd_voip_help.arg_help)

    ########################################################################
    # supporting methods
    #
    def _human_repr2id(self, human_repr):
        """Just like the superclass, except mac addresses are trapped here."""

        if (isinstance(human_repr, (str, unicode)) and len(human_repr) == 17
                and all(x.lower() in "0123456789abcdef: "
                        for x in human_repr)):
            return "mac", human_repr

        return super(BofhdVoipCommands, self)._human_repr2id(human_repr)

    def _get_entity_id(self, designation):
        """Check whetner L{designation} may be interpreted as an entity id.

        Return the id, if possible, or None, when no conversion applies. 

        We consider the following as entity_id:

          + sequence of digits
          + string with sequence of digits
          + string of the form 'id:digits'
        """
        id_type, value = self._human_repr2id(designation)
        if id_type == "id":
            return int(value)
        return None

    def _is_numeric_id(self, value):
        return (isinstance(value, (int, long))
                or isinstance(value, (str, unicode)) and value.isdigit())

    def _get_constant(self, designation, const_type):
        """Fetch a single constant based on some human designation.

        Return the first match given by 'designation' (see
        Constants.py:fetch_constants).

        FIXME: refactor this to bofhd_core.py ?
        """

        cnst = self.const.fetch_constants(const_type, designation)
        if len(cnst) == 0:
            raise CerebrumError("Unknown %s constant: %s" %
                                (str(const_type), designation))
        return cnst[0]

    def _get_ou(self, designation):
        """Fetch a single OU identified by designation.

        TODO: extend this method to be generous about what's accepted as
        input.
        TODO: how do we decide if 123456 means sko or entity_id?
        FIXME: compare this with other incarnations of _get_ou. Maybe there
               can be ONE method that covers all the cases with an obvious
               interface? E.g. we should accept things like 33-15-20 as it is
               OBVIOUSLY a sko.
        """

        id_type, value = self._human_repr2id(designation)
        if str(value).isdigit():
            for key in ("ou_id", "stedkode"):
                try:
                    x = super(BofhdVoipCommands,
                              self)._get_ou(**{key: str(value)})
                    return x
                except (Errors.NotFoundError, CerebrumError):
                    pass

        raise CerebrumError("Could not find ou by designation %s" %
                            str(designation))

    def _get_voip_service(self, designation):
        """Locate a specific voip_service.

        We try to be a bit lax when it comes to identifying voip_services. A
        numeric designation is interpreted as entity_id. Everything else is
        interpreted as description.
        """

        service = VoipService(self.db)
        if self._get_entity_id(designation):
            try:
                service.find(self._get_entity_id(designation))
                return service
            except Errors.NotFoundError:
                raise CerebrumError("Could not find voip service with id=%s" %
                                    str(designation))

        ids = service.search_voip_service_by_description(designation,
                                                         exact_match=True)
        if len(ids) == 1:
            service.find(ids[0]["entity_id"])
            return service

        raise CerebrumError("Could not uniquely determine voip_service "
                            "from description %s" % str(designation))

    def _get_voip_address_by_service_description(self, designation):
        try:
            service = self._get_voip_service(designation)
            return self._get_voip_address_by_owner_entity_id(service.entity_id)
        except (CerebrumError, Errors.NotFoundError):
            return list()

    def _get_voip_address_by_owner_account(self, designation):
        try:
            account = self._get_account(designation)
            return self._get_voip_address_by_owner_entity_id(account.owner_id)
        except (CerebrumError, Errors.NotFoundError):
            return list()

    def _get_voip_address_by_contact_info(self, designation):
        try:
            ids = set(x["entity_id"]
                      for x in self._get_contact_info(designation))
            result = list()
            for x in ids:
                result.extend(self._get_voip_address_by_owner_entity_id(x))
            return result
        except Errors.NotFoundError:
            return list()

        assert False, "NOTREACHED"

    def _get_voip_address_by_owner_entity_id(self, designation):
        if not self._is_numeric_id(designation):
            return list()

        value = int(designation)
        try:
            va = VoipAddress(self.db)
            va.find_by_owner_id(value)
            return [
                va,
            ]
        except Errors.NotFoundError:
            return list()

        assert False, "NOTREACHED"

    def _get_voip_address_by_entity_id(self, designation):
        """Return all voip_addresses matching the specified entity_id."""

        if not self._is_numeric_id(designation):
            return list()

        value = int(designation)
        try:
            va = VoipAddress(self.db)
            va.find(value)
            return [
                va,
            ]
        except Errors.NotFoundError:
            return list()

        assert False, "NOTREACHED"

    def _get_voip_address(self, designation, all_matches=False):
        """Collect voipAddress instance(s) matching designation.

        Crap. This turned out to be extremely complicated, since there are so
        many ways we could interpret the meaning of 'give a voip_address for
        <doohickey>'.
        
        @param all_matches:
          Controls whether to collect all possible matches, or just the first
          one. Note that the default case is to collect the single voipAddress
          that matches the first of the search criteria below. Even though
          previous searches may have matched multiple addresses, the first
          search yielding exactly one answer will be used. 
        """

        search_names = (
            "by_entity_id",
            "by_owner_entity_id",
            "by_contact_info",
            "by_owner_account",
            "by_service_description",
        )
        id_type, value = self._human_repr2id(designation)
        result = list()
        collected_ids = set()
        for partial_name in search_names:
            caller_name = "_get_voip_address_" + partial_name
            caller = getattr(self, caller_name)
            addrs = caller(value)
            # self.logger.debug("Searcher %s returned %d VA(s)", caller_name,
            #                   len(addrs))
            result.extend(x for x in addrs if x.entity_id not in collected_ids)
            collected_ids.update(x.entity_id for x in addrs)
            # If an exact match is requested, grab the first search that yields
            # exactly 1 answer.
            if not all_matches and len(addrs) == 1:
                return addrs[0]

        if len(result) == 0:
            raise CerebrumError("No voip_address matches designation %s" %
                                (designation, ))
        if not all_matches and len(result) > 1:
            raise CerebrumError(
                "Cannot uniquely determine voip_address from "
                "designation %s: matching ids=%s" %
                (designation, ", ".join(str(x.entity_id) for x in result)))
        return result

    def _get_or_create_voip_address(self, owner_id, with_softphone=True):
        """Much like _get_voip_address(), except this one creates it as well
        if it does not exist, rather than failing.

        with_softphone controls whether we want to create a softphone for the
        new voip_address, should voip_address be created.
        """

        address = VoipAddress(self.db)
        try:
            address.find_by_owner_id(owner_id)
        except Errors.NotFoundError:
            address.clear()
            address.populate(owner_id)
            address.write_db()
            address.write_db()

            if with_softphone:
                self._create_default_softphone_client(address.entity_id)
        return address

    def _create_default_softphone_client(self, voip_address_id):
        """Help function to create default softphone client for all addresses
        and services.
        """

        #
        # If it exists, we are done...
        client = VoipClient(self.db)
        if client.search(voip_address_id=voip_address_id,
                         client_type=self.const.voip_client_type_softphone):
            return

        client.populate(
            voip_address_id,
            self.const.voip_client_type_softphone,
            True,  # sip_enabled by default
            None,  # softphones don't have MACs
            self.const.voip_client_info_softphone)
        client.write_db()
        client.set_auth_data(self.const.voip_auth_sip_secret,
                             client.generate_sip_secret())
        self.logger.debug(
            "Automatically generated softphone "
            "client id=%s for address %s", client.entity_id, voip_address_id)

    def _get_voip_client(self, designation):
        """Locate a voip_client by designation.

        Possible interpretations of designation are:

          + entity_id
          + mac address
        """

        # Don't use _human_repr2id here, since it does not like ':' being part
        # of the identifier (which mac addresses definitely have)
        client = VoipClient(self.db)
        if (isinstance(designation, (int, long))
                or isinstance(designation, str) and designation.isdigit()):
            try:
                client.find(int(designation))
                return client
            except Errors.NotFoundError:
                pass

        # Try to look up by mac address
        try:
            client.clear()
            client.find_by_mac_address(designation)
            return client
        except (Errors.NotFoundError, AssertionError):
            pass

        raise CerebrumError("Could not uniquely determine voip_client "
                            "from designation %s" % str(designation))

    def _get_voip_person(self, designation):
        """Lookup a person by <something>.
        """

        person = Factory.get("Person")(self.db)
        if self._get_entity_id(designation):
            try:
                person.find(self._get_entity_id(designation))
                return person
            except Errors.NotFoundError:
                pass

        # fnr?
        exc = CerebrumError("No person found for designation %s" %
                            str(designation))
        id_type, value = self._human_repr2id(designation)
        if str(value).isdigit():
            try:
                fnr = personnr_ok(str(value))
                person.find_by_external_id(self.const.externalid_fodselsnr,
                                           fnr)
                return person
            except (InvalidFnrError, Errors.NotFoundError):
                pass

        # account?
        try:
            account = Factory.get("Account")(self.db)
            account.find_by_name(str(value))
            if account.owner_type == self.const.entity_person:
                return self._get_voip_person(account.owner_id)
        except Errors.NotFoundError:
            pass

        # By other external ids? By name?
        raise exc

    def _get_voip_owner(self, designation):
        """Locate the owner of a voip address.

        We try in order to look up voip-service and then person. If the search
        is unsuccessful, throw CerebrumError.

        @param designation:
          Some sort of identification of the owner.

        @return:
          A voipService/Person instance, or raise CerebrumError if nothing
          appropriate is found.
        """

        for method in (self._get_voip_person, self._get_voip_service):
            try:
                return method(designation)
            except CerebrumError:
                pass

        raise CerebrumError("Cannot locate person/voip-service designated "
                            "by %s" % str(designation))

    def _get_contact_info(self, value):
        """Return a sequence of dicts containing entity_contact_info entries
        for the specified contact value.

        We scan system_voip ONLY.

        value is matched both against contact_value and contact_alias.
        """

        result = list()
        eci = EntityContactInfo(self.db)
        for row in eci.list_contact_info(source_system=self.const.system_voip,
                                         contact_value=str(value)):
            result.append(row.dict())

        for row in eci.list_contact_info(source_system=self.const.system_voip,
                                         contact_alias=str(value)):
            result.append(row.dict())

        return result

    def _collect_constants(self, const_type):
        """Return a suitable data structure containing all constants of the
        given type. 
        """

        result = list()
        for cnst in self.const.fetch_constants(const_type):
            result.append({
                "code": int(cnst),
                "code_str": str(cnst),
                "description": cnst.description
            })
        return sorted(result, key=lambda r: r["code_str"])

    def _typeset_traits(self, trait_sequence):
        """Return a human-friendly version of entity traits.
        """

        traits = list()
        for et in trait_sequence:
            traits.append("%s" % (str(self.const.EntityTrait(et)), ))

        return traits

    def _typeset_ou(self, ou_id):
        """Return a human-friendly description of an OU.

        Return something like '33-15-20 USIT'
        """
        ou = self._get_ou(int(ou_id))
        # Use acronym, if available, otherwise use short-name
        acronym = ou.get_name_with_language(self.const.ou_name_acronym,
                                            self.const.language_nb, None)
        short_name = ou.get_name_with_language(self.const.ou_name_short,
                                               self.const.language_nb, None)
        ou_name = acronym and acronym or short_name

        location = "%02d-%02d-%02d (%s)" % (ou.fakultet, ou.institutt,
                                            ou.avdeling, ou_name)
        return location

    def _typeset_bool(self, value):
        """Return a human-friendly version of a boolean.
        """

        return bool(value)

    def _assert_unused_service_description(self, description):
        """Check that a description is not in use by any voip_service.
        """
        vs = VoipService(self.db)
        services = vs.search_voip_service_by_description(description,
                                                         exact_match=True)
        if services:
            raise CerebrumError("Description must be unique. In use by id=%s." \
                                % services[0]['entity_id'])

    ########################################################################
    # voip_service related commands
    #
    all_commands["voip_service_new"] = Command(("voip", "service_new"),
                                               VoipServiceTypeCode(), OU(),
                                               SimpleString())

    def voip_service_new(self, operator, service_type, ou_tag, description):
        """Create a new voip_service.

        @param service_type: Type of voip_service entry.

        @param ou_tag: OU where the voip_service located (stedkode)

        @param description: service's description. Must be globally unique.
        """

        self.ba.can_create_voip_service(operator.get_entity_id())
        self._assert_unused_service_description(description)
        service = VoipService(self.db)

        ou = self._get_ou(ou_tag)
        service_type = self._get_constant(service_type,
                                          self.const.VoipServiceTypeCode)
        if service_type is None:
            raise CerebrumError("Unknown voip_service_type: %s" %
                                str(service_type))
        service.populate(description, service_type, ou.entity_id)
        service.write_db()

        # Create a corresponding voip_address...
        self._get_or_create_voip_address(service.entity_id)
        return "OK, new voip_service (%s), entity_id=%s" % (str(service_type),
                                                            service.entity_id)

    # end voip_service_new

    all_commands["voip_service_info"] = Command(("voip", "service_info"),
                                                VoipServiceParameter(),
                                                fs=FormatSuggestion(
                                                    "Entity id:     %d\n"
                                                    "Service type:  %s\n"
                                                    "Description:   %s\n"
                                                    "Location:      %s\n"
                                                    "Traits:        %s\n", (
                                                        "entity_id",
                                                        "service_type",
                                                        "description",
                                                        "location",
                                                        "traits",
                                                    )))

    def voip_service_info(self, operator, designation):
        """Return information about a voip_service.

        @param designation: either an entity_id or a description.
        """

        self.ba.can_view_voip_service(operator.get_entity_id())
        service = self._get_voip_service(designation)
        answer = {
            "entity_id":
            service.entity_id,
            "service_type":
            str(self.const.VoipServiceTypeCode(service.service_type)),
            "description":
            service.description,
        }
        answer["location"] = self._typeset_ou(service.ou_id)
        answer["traits"] = self._typeset_traits(service.get_traits())
        return answer

    # end voip_service_info

    all_commands["voip_service_type_list"] = Command(
        ("voip", "service_type_list"),
        fs=FormatSuggestion("%-32s  %s", ("code_str", "description"),
                            hdr="%-32s  %s" % ("Type", "Description")))

    def voip_service_type_list(self, operator):
        """List available service_info_code values."""

        return self._collect_constants(self.const.VoipServiceTypeCode)

    # end voip_service_type_list

    all_commands["voip_service_delete"] = Command(("voip", "service_delete"),
                                                  VoipServiceParameter())

    def voip_service_delete(self, operator, designation):
        """Delete the specified voip_service.
        
        Caveat: this method assumes that the related voip_address/clients have
        already been deleted. This is by design.
        """

        self.ba.can_create_voip_service(operator.get_entity_id())
        service = self._get_voip_service(designation)
        entity_id, description = service.entity_id, service.description
        service.delete()
        return "OK, deleted voip_service id=%s, %s" % (entity_id, description)

    # end voip_service_delete

    all_commands["voip_service_update"] = Command(
        ("voip", "service_update"), VoipServiceParameter(),
        SimpleString(optional=True, default=None),
        VoipServiceTypeCode(optional=True, default=None),
        OU(optional=True, default=None))

    def voip_service_update(self,
                            operator,
                            designation,
                            description=None,
                            service_type=None,
                            ou_tag=None):
        """Update information about an existing voip_service.

        Those attributes that are None are left alone.
        """

        self.ba.can_alter_voip_service(operator.get_entity_id())
        service = self._get_voip_service(designation)

        if description and service.description != description:
            self._assert_unused_service_description(description)
            service.description = description
        if service_type:
            service_type = self._get_constant(service_type,
                                              self.const.VoipServiceTypeCode)
            if service.service_type != service_type:
                service.service_type = service_type
        if ou_tag:
            ou = self._get_ou(ou_tag)
            if service.ou_id != ou.entity_id:
                service.ou_id = ou.entity_id

        service.write_db()
        return "OK, updated information for voip_service id=%s" % (
            service.entity_id, )

    # end voip_service_update

    all_commands["voip_service_find"] = Command(
        ("voip", "service_find"),
        SimpleString(),
        fs=FormatSuggestion("%8i   %15s   %25s   %8s",
                            ("entity_id", "description", "service_type", "ou"),
                            hdr="%8s   %15s   %25s   %8s" %
                            ("EntityId", "Description", "Type", "Stedkode")))

    def voip_service_find(self, operator, designation):
        """List all voip_services matched in some way by designation.

        This has been requested to ease up looking up voip_services for users.

        designation is used to look up voip_services in the following fashion:
        
          - if all digits -> by entity_id
          - by description (exactly)
          - by description (substring search)
          - if all digits -> by ou_id
          - if all digits -> by stedkode

        All the matching voip_services are collected and returned as a sequence
        so people can pluck out the entities they want and use them in
        subsequent commands. 
        """
        def fold_description(s):
            cutoff = 15
            suffix = "(...)"
            if len(s) > cutoff:
                return s[:(cutoff - len(suffix))] + suffix
            return s

        # end fold_description

        self.ba.can_view_voip_service(operator.get_entity_id())
        ident = designation.strip()
        collect = dict()
        vs = VoipService(self.db)

        # let's try by-id lookup first
        if ident.isdigit():
            try:
                vs.find(int(ident))
                collect[vs.entity_id] = (vs.description, vs.service_type,
                                         vs.ou_id)
            except Errors.NotFoundError:
                pass

        # then by-description...
        for exact_match in (False, True):
            results = vs.search_voip_service_by_description(
                designation, exact_match=exact_match)
            for row in results:
                collect[row["entity_id"]] = (row["description"],
                                             row["service_type"], row["ou_id"])

        # then by OU (stedkode and ou_id)
        try:
            ou = self._get_ou(designation)
            for row in vs.search(ou_id=ou.entity_id):
                collect[row["entity_id"]] = (row["description"],
                                             row["service_type"], row["ou_id"])
        except CerebrumError:
            pass

        # Finally, the presentation layer
        if len(collect) > cereconf.BOFHD_MAX_MATCHES:
            raise CerebrumError("More than %d (%d) matches, please narrow "
                                "search criteria" %
                                (cereconf.BOFHD_MAX_MATCHES, len(collect)))

        answer = list()
        for entity_id in collect:
            description, service_type, ou_id = collect[entity_id]
            answer.append({
                "entity_id":
                entity_id,
                "description":
                fold_description(description),
                "service_type":
                str(self.const.VoipServiceTypeCode(service_type)),
                "ou":
                self._typeset_ou(ou_id),
            })
        return answer

    # end voip_service_find

    ########################################################################
    # voip_client related commands

    #
    # voip TODO
    #
    all_commands["voip_client_new"] = Command(
        ("voip", "client_new"), VoipOwnerParameter(), VoipClientTypeCode(),
        MacAddress(), VoipClientInfoCode(),
        YesNo(help_ref='yes_no_sip_enabled', default="Yes"))

    def voip_client_new(self,
                        operator,
                        owner_designation,
                        client_type,
                        mac_address,
                        client_info,
                        sip_enabled=True):
        """Create a new voip_client.

        If the owner (be it voip_service or person) does NOT have a
        voip_address, create that as well.
        """

        self.ba.can_create_voip_client(operator.get_entity_id())
        # Find the owner first...
        owner = self._get_voip_owner(owner_designation)

        if isinstance(sip_enabled, (str, unicode)):
            sip_enabled = self._get_boolean(sip_enabled)

        # Does that mac_address point to something?
        try:
            self._get_voip_client(mac_address)
            raise CerebrumError("Mac address %s is already bound to a "
                                "voip_client." % str(mac_address))
        except CerebrumError:
            pass

        # Check that info/type_code make sense...
        ct = self._get_constant(client_type, self.const.VoipClientTypeCode)
        ci = self._get_constant(client_info, self.const.VoipClientInfoCode)

        if not ((ct == self.const.voip_client_type_softphone
                 and not mac_address) or
                (ct == self.const.voip_client_type_hardphone and mac_address)):
            raise CerebrumError("Hardphones must have mac; softphones must "
                                "not: %s -> %s" % (str(ct), mac_address))

        # get/create an address for that owner already...
        address = self._get_or_create_voip_address(
            owner.entity_id,
            with_softphone=(ct == self.const.voip_client_type_softphone))

        client = VoipClient(self.db)
        client.populate(address.entity_id, ct, sip_enabled, mac_address, ci)
        client.write_db()
        client.set_auth_data(self.const.voip_auth_sip_secret,
                             client.generate_sip_secret())
        return "OK, created voipClient %s, id=%s" % (str(ct), client.entity_id)

    #
    # voip TODO
    #
    all_commands["voip_client_info"] = Command(
        ("voip", "client_info"),
        VoipClientParameter(),
        fs=FormatSuggestion(
            "Entity id:              %d\n"
            "Client type:            %s\n"
            "Client info:            %s\n"
            "Mac address:            %s\n"
            "sip enabled:            %s\n"
            "has secret?:            %s\n"
            "Traits:                 %s\n"
            "voipAddress id:         %d\n"
            "voipAddress' owner:     %s\n",
            ("entity_id", "client_type", "client_info", "mac_address",
             "sip_enabled", "has_secret", "traits", "voip_address_id",
             "owner")))

    def voip_client_info(self, operator, designation):
        """Return information about a voip_client.
        """

        self.ba.can_view_voip_client(operator.get_entity_id())
        client = self._get_voip_client(designation)
        address = self._get_voip_address(client.voip_address_id)

        # This is such a bloody overkill... We just need id/name But f**k it,
        # CPU cycles are cheap.
        address_attrs = address.get_voip_attributes()
        client_attrs = client.get_voip_attributes()

        owner = "cn=%s (id=%s)" % (address_attrs["cn"],
                                   address.owner_entity_id)
        answer = {
            "entity_id":
            client.entity_id,
            "client_type":
            client_attrs["sipClientType"],
            "client_info":
            client_attrs["sipClientInfo"],
            "mac_address":
            client_attrs["sipMacAddress"],
            "sip_enabled":
            self._typeset_bool(client_attrs["sipEnabled"]),
            "has_secret":
            bool(client.get_auth_data(self.const.voip_auth_sip_secret)),
            "traits":
            self._typeset_traits(client.get_traits()),
            "voip_address_id":
            client.voip_address_id,
            "owner":
            owner,
        }
        return answer

    #
    # voip TODO
    #
    all_commands["voip_client_list_info_code"] = Command(
        ("voip", "client_list_info_code"),
        fs=FormatSuggestion("%-25s  %s", ("code_str", "description"),
                            hdr="%-25s  %s" %
                            ("Client info code", "Description")))

    def voip_client_list_info_code(self, operator):
        """List all possible voip_client info codes."""
        return self._collect_constants(self.const.VoipClientInfoCode)

    #
    # voip TODO
    #
    all_commands["voip_client_list_type_code"] = Command(
        ("voip", "client_list_type_code"),
        fs=FormatSuggestion("%-25s  %s", ("code_str", "description"),
                            hdr="%-25s  %s" %
                            ("Client type code", "Description")))

    def voip_client_list_type_code(self, operator):
        """List all possible voip_client type codes.
        """
        return self._collect_constants(self.const.VoipClientTypeCode)

    #
    # voip TODO
    #
    all_commands["voip_client_delete"] = Command(("voip", "client_delete"),
                                                 VoipClientParameter())

    def voip_client_delete(self, operator, designation):
        """Remove (completely) a voip_client from Cerebrum."""

        self.ba.can_create_voip_client(operator.get_entity_id())
        client = self._get_voip_client(designation)
        entity_id, mac = client.entity_id, client.mac_address
        client.delete()
        return "OK, removed voipClient id=%s (mac=%s)" % (entity_id, mac)

    #
    # voip TODO
    #
    all_commands["voip_client_set_info_code"] = Command(
        ("voip", "client_set_info_code"), VoipClientParameter(),
        VoipClientInfoCode())

    def voip_client_set_info_code(self, operator, designation, new_info):
        """Change client_info for a specified client."""
        self.ba.can_alter_voip_client(operator.get_entity_id())

        ci = self._get_constant(new_info, self.const.VoipClientInfoCode)

        client = self._get_voip_client(designation)
        if client.client_type != self.const.voip_client_type_hardphone:
            raise CerebrumError("Can only change hardphones.")

        client.client_info = ci
        client.write_db()
        return "OK, changed for voipClient id=%s" % client.entity_id

    #
    # voip client_sip_enabled <client> yes|no
    #
    all_commands["voip_client_sip_enabled"] = Command(
        ("voip", "client_sip_enabled"), VoipClientParameter(),
        YesNo(help_ref='yes_no_sip_enabled'))

    def voip_client_sip_enabled(self, operator, designation, yesno):
        """Set sip_enabled to True/False for a specified client."""

        self.ba.can_alter_voip_client(operator.get_entity_id())

        client = self._get_voip_client(designation)
        status = self._get_boolean(yesno)

        if client.sip_enabled == status:
            return "OK (no changes for client id=%s)" % (client.entity_id)

        client.sip_enabled = status
        client.write_db()
        return "OK (changed sip_enabled to %s for client id=%s)" % (
            client.sip_enabled, client.entity_id)

    #
    # voip client_secrets_reset <client>
    #
    all_commands["voip_client_secrets_reset"] = Command(
        ("voip", "client_secrets_reset"), VoipClientParameter())

    def voip_client_secrets_reset(self, operator, designation):
        """Reset all of voip_client's secrets.

        This is useful if a client has been compromised and needs to be reset.
        """

        client = self._get_voip_client(designation)
        self.ba.can_reset_client_secrets(operator.get_entity_id(),
                                         client.entity_id)
        for secret_kind in (self.const.voip_auth_sip_secret, ):
            secret = client.generate_sip_secret()
            client.set_auth_data(secret_kind, secret)

        return "OK (reset sip secrets for voip_client id=%s)" % (
            client.entity_id, )

    # end voip_client_secrets_reset

    #
    # voip client_new_secret <client> <secret>
    #
    all_commands["voip_client_new_secret"] = Command(
        ("voip", "client_new_secret"), VoipClientParameter(), SimpleString())

    def voip_client_new_secret(self, operator, designation, new_secret):
        """Register a new sipSecret for the specified client."""
        client = self._get_voip_client(designation)
        self.ba.can_set_new_secret(operator.get_entity_id(), client.entity_id)

        # First check the new_secret quality.
        client.validate_auth_data(self.const.voip_auth_sip_secret, new_secret)
        # Locate current secret
        current_secret = client.get_auth_data(self.const.voip_auth_sip_secret)

        # Register new_secret
        client.set_auth_data(self.const.voip_auth_sip_secret, new_secret)

        return "OK (set new sip secret for voip_client id=%s)" % (
            client.entity_id, )

    ########################################################################
    # voip_address related commands

    #
    # voip address_list_contact_codes
    #
    all_commands["voip_address_list_contact_codes"] = Command(
        ("voip", "address_list_contact_codes"),
        fs=FormatSuggestion("%-25s  %s", ("code_str", "description"),
                            hdr="%-25s  %s" % ("Code", "Description")))

    def voip_address_list_contact_codes(self, operator):
        """List all available contact_info_codes.
        """
        return self._collect_constants(self.const.ContactInfo)

    #
    # voip address_add_number <owner> <ctype> <TODO> [TODO]
    #
    all_commands["voip_address_add_number"] = Command(
        ("voip", "address_add_number"), VoipOwnerParameter(),
        ContactTypeParameter(), SimpleString(help_ref="voip_extension_full"),
        PriorityParameter(optional=True, default=None))

    def voip_address_add_number(self,
                                operator,
                                designation,
                                contact_type,
                                contact_full,
                                priority=None):
        """Add a new phone number to a voip_address at given priority.

        The number is added, assuming that no voip_address already owns either
        full or internal number. internal_number must be set.

        If no priority is specified, one is chosen automatically (it would be
        the lowest possible priority)
        """
        def owner_has_contact(owner, contact_value):
            entity_ids = [
                x["entity_id"] for x in self._get_contact_info(contact_value)
            ]
            return owner.entity_id in entity_ids

        # end owner_has_contact

        def next_priority(owner, priority=None):
            if priority is None:
                so_far = owner.list_contact_info(
                    entity_id=owner.entity_id,
                    source_system=self.const.system_voip)
                result = 1
                if so_far:
                    result = max(x["contact_pref"] for x in so_far) + 1
                return result

            priority = int(priority)
            if priority <= 0:
                raise CerebrumError("Priority must be larger than 0")
            return priority

        # end next_priority

        self.ba.can_alter_number(operator.get_entity_id())

        # Make sure that the number is not in use.
        if self._get_contact_info(contact_full):
            raise CerebrumError("Number %s already in use." % contact_full)

        # Is there an address for that owner already?
        owner = self._get_voip_owner(designation)
        address = self._get_or_create_voip_address(owner.entity_id)

        if contact_type:
            contact_type = self._get_constant(contact_type,
                                              self.const.ContactInfo)
        else:
            contact_type = self.const.contact_voip_extension

        # Deduce 5-digit alias, if contact info is of the proper type.
        contact_alias = None
        if contact_type == self.const.contact_voip_extension:
            contact_alias = contact_full[-5:]

        # Check the numbers for syntax errors
        if not address.contact_is_valid(contact_type, contact_full,
                                        contact_alias):
            raise CerebrumError("(%s, %s) number pair is not a valid number." %
                                (contact_full, contact_alias))

        # Have these numbers already been registered to owner?
        if (owner_has_contact(owner, contact_full)
                or owner_has_contact(owner, contact_alias)):
            raise CerebrumError("%s or %s has already been registered "
                                "for %s." %
                                (contact_full, contact_alias, owner.entity_id))

        # Figure out the priority if it has not been specified
        priority = next_priority(owner, priority)
        owner.add_contact_info(self.const.system_voip,
                               contact_type,
                               contact_full,
                               pref=priority,
                               alias=contact_alias)
        warn = "."
        if contact_alias and not contact_full.endswith(contact_alias):
            warn = " (Short number does not match full number)."

        return "OK, associated %s/%s with owner id=%s%s" % (
            contact_full, contact_alias, owner.entity_id, warn)

    #
    # voip address_delete_number <TODO> [TODO]
    #
    all_commands["voip_address_delete_number"] = Command(
        ("voip", "address_delete_number"),
        SimpleString(help_ref="voip_extension_full"),
        VoipOwnerParameter(optional=True, default=None))

    def voip_address_delete_number(self, operator, value, owner=None):
        """Delete a previously registered phone number.

        If a phone number is associated with several entities, we cowardly
        refuse, unless owner is specified.

        @param value:
          Contact info string. Could be full or short version of the phone
          number.
        """

        self.ba.can_alter_number(operator.get_entity_id())
        contacts = self._get_contact_info(value)
        if not contacts:
            return "OK, nothing to delete (no record of %s)" % (str(value))

        if owner is not None:
            owner = self._get_voip_owner(owner)

        if len(contacts) > 1 and owner is None:
            raise CerebrumError("Multiple entities have %s registered. You "
                                "must specified entity to update." % (value, ))

        eci = EntityContactInfo(self.db)
        victims = set()
        for d in contacts:
            if owner and owner.entity_id != d["entity_id"]:
                continue

            eci.clear()
            eci.find(d["entity_id"])
            eci.delete_contact_info(d["source_system"], d["contact_type"],
                                    d["contact_pref"])
            victims.add(eci.entity_id)

        return "OK, removed %s from %d entit%s: %s" % (
            str(value), len(victims), len(victims) != 1 and "ies"
            or "y", ", ".join("id=%s" % x for x in victims))

    #
    # voip address_info <voip-addr>
    #
    all_commands["voip_address_info"] = Command(
        ("voip", "address_info"),
        VoipAddressParameter(),
        fs=FormatSuggestion(
            "Entity id:              %d\n"
            "Owner entity id:        %d\n"
            "Owner type:             %s\n"
            "cn:                     %s\n"
            "sipURI:                 %s\n"
            "sipPrimaryURI:          %s\n"
            "e164URI:                %s\n"
            "Extension URI:          %s\n"
            "Traits:                 %s\n"
            "Clients:                %s\n", (
                "entity_id",
                "owner_entity_id",
                "owner_entity_type",
                "cn",
                "sip_uri",
                "sip_primary_uri",
                "e164_uri",
                "extension_uri",
                "traits",
                "clients",
            )))

    def voip_address_info(self, operator, designation):
        """Display information about ... ?

        The spec says 'all attributes': uname, cn, all URIs, e-mail, etc.

        @param designation:
          uid, id, fnr -- some way of identifying the proper voip-address.
          FIXME: Should be split designation into key:value, where key is one
          of (uname, cn, phone, entity_id/id) and value is the string
          interpreted according to the meaning of the first key.
        """

        address = self._get_voip_address(designation)
        owner = address.get_owner()

        # find the clients
        client = VoipClient(self.db)
        client_ids = sorted([
            str(x["entity_id"])
            for x in client.search(voip_address_id=address.entity_id)
        ])

        attrs = address.get_voip_attributes()
        result = {
            "entity_id": address.entity_id,
            "owner_entity_id": owner.entity_id,
            "owner_entity_type": str(self.const.EntityType(owner.entity_type)),
            "cn": attrs["cn"],
            "sip_uri": attrs["voipSipUri"],
            "sip_primary_uri": attrs["voipSipPrimaryUri"],
            "e164_uri": attrs["voipE164Uri"],
            "extension_uri": attrs["voipExtensionUri"],
            "traits": self._typeset_traits(address.get_traits()),
            "clients": client_ids,
        }

        return result

    #
    # void address_delete <voip-addr>
    #
    all_commands["voip_address_delete"] = Command(("voip", "address_delete"),
                                                  VoipAddressParameter())

    def voip_address_delete(self, operator, designation):
        """Delete a voip_address from Cerebrum.

        This is useful as a precursor to removing a service.
        """

        self.ba.can_alter_voip_address(operator.get_entity_id())
        address = self._get_voip_address(designation)

        # Without this check users risk seeing an internal error, rather than
        # a detailed error message.
        client = VoipClient(self.db)
        clients = list(client.search(voip_address_id=address.entity_id))
        if clients:
            raise CerebrumError("Won't delete address id=%s: "
                                "it has %d voip_client(s)" %
                                (address.entity_id, len(clients)))

        address_id = address.entity_id
        owner = address.get_owner()
        address.delete()
        return "OK, deleted voip_address id=%s (owned by %s id=%s)" % (
            address_id, self.const.EntityType(
                owner.entity_type), owner.entity_id)

    #
    # voip address_find <search-param>
    #
    all_commands["voip_address_find"] = Command(
        ("voip", "address_find"),
        SimpleString(),
        fs=FormatSuggestion(
            "%9i %14i %13s %25s",
            ("entity_id", "owner_entity_id", "owner_type", "cn"),
            hdr="%9s %14s %13s %25s" %
            ("EntityId", "OwnerEntityId", "Owner type", "CN")))

    def voip_address_find(self, operator, designation):
        """List all voip_addresses matched in some way by designation.

        This has been requested to ease up searching for specific
        voip_addresses.
        """
        vas = self._get_voip_address(designation, all_matches=True)
        # Finally, the presentation layer
        if len(vas) > cereconf.BOFHD_MAX_MATCHES:
            raise CerebrumError("More than %d (%d) matches, please narrow down"
                                "the search criteria" %
                                (cereconf.BOFHD_MAX_MATCHES, len(vas)))
        answer = list()
        for va in vas:
            # NB! This call is pretty expensive. We may want to revise
            # BOFHD_MAX_MATCHES-cutoff here (and rather replace it with
            # something more stringent)
            voip_attrs = va.get_voip_attributes()
            answer.append({
                "entity_id": va.entity_id,
                "owner_entity_id": va.owner_entity_id,
                "owner_type": str(voip_attrs["owner_type"]),
                "cn": voip_attrs["cn"],
            })
        return answer
示例#4
0
class BofhdJobRunnerCommands(BofhdCommandBase):
    """Bofh commands for job_runner."""

    all_commands = {}
    authz = BofhdJobRunnerAuth

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        return CMD_GROUP, CMD_HELP, CMD_ARGS

    def _run_job_runner_command(self, command, args=None):
        """Run a job_runner command via the job_runner socket."""
        with closing(socket.socket(socket.AF_UNIX)) as sock:
            sock.connect(cereconf.JOB_RUNNER_SOCKET)
            sock.settimeout(0.2)
            try:
                return SocketProtocol.call(SocketConnection(sock), command,
                                           args)
            except socket.timeout:
                raise CerebrumError('Error talking to job_runner. Socket '
                                    'timeout')

    #
    # job_runner status
    #
    all_commands['job_runner_status'] = Command(
        ("job_runner", "status"),
        fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'),
        perm_filter='can_show_job_runner_status')

    def job_runner_status(self, operator):
        """Show job runner status."""
        # Access control
        self.ba.can_show_job_runner_status(operator.get_entity_id())
        return self._run_job_runner_command('STATUS')

    # job_runner info
    #
    all_commands['job_runner_info'] = Command(
        ("job_runner", "info"),
        SimpleString(help_ref='job_runner_name', repeat=False),
        fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'),
        perm_filter='can_show_job_runner_job')

    def job_runner_info(self, operator, job_name):
        """Show job runner status."""
        # Access control
        self.ba.can_show_job_runner_job(operator.get_entity_id())
        return self._run_job_runner_command('SHOWJOB', [
            job_name,
        ])

    #
    # job_runner run
    #
    all_commands['job_runner_run'] = Command(
        ("job_runner", "run"),
        SimpleString(help_ref='job_runner_name', repeat=False),
        YesNo(help_ref='yes_no_with_deps', optional=True, default="No"),
        fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'),
        perm_filter='can_run_job_runner_job')

    def job_runner_run(self, operator, job_name, with_deps=False):
        """Run a job runner job."""
        # Access control
        self.ba.can_run_job_runner_job(operator.get_entity_id())
        with_deps = self._get_boolean(with_deps)
        return self._run_job_runner_command('RUNJOB', [job_name, with_deps])
class BofhdExtension(BofhdCommandBase):
    """Class to expand bofhd with commands for manipulating subnets."""

    all_commands = {}
    parent_commands = False
    authz = SubnetBofhdAuth

    def __init__(self, *args, **kwargs):
        default_zone = getattr(cereconf, 'DNS_DEFAULT_ZONE',
                               kwargs.pop('default_zone', 'uio'))
        super(BofhdExtension, self).__init__(*args, **kwargs)
        self.default_zone = self.const.DnsZone(default_zone)

    @property
    def _find(self):
        try:
            return self.__find_util
        except AttributeError:
            self.__find_util = Utils.Find(self.db, self.default_zone)
            return self.__find_util

    def _get_subnet_ipv4(self, subnet_identifier):
        try:
            s = Subnet(self.db)
            s.find(subnet_identifier)
            return s
        except (ValueError, SubnetError, DNSError):
            raise CerebrumError("Unable to find subnet %r" % subnet_identifier)

    def _get_subnet_ipv6(self, subnet_identifier):
        try:
            s = IPv6Subnet(self.db)
            s.find(subnet_identifier)
            return s
        except (ValueError, SubnetError, DNSError):
            raise CerebrumError("Unable to find subnet %r" % subnet_identifier)

    def _get_subnet(self, subnet_identifier):
        try:
            return self._get_subnet_ipv4(subnet_identifier)
        except CerebrumError:
            return self._get_subnet_ipv6(subnet_identifier)

    @classmethod
    def get_help_strings(cls):
        _, _, args = get_help_strings()
        return merge_help_strings(
            ({}, {}, args),
            (HELP_SUBNET_GROUP, HELP_SUBNET_CMDS, HELP_SUBNET_ARGS))

    #
    # subnet info <subnet>
    #
    all_commands['subnet_info'] = Command(
        ("subnet", "info"),
        SubnetIdentifier(),
        fs=FormatSuggestion([
            ("Subnet:                 %s", ('subnet', )),
            ("Entity ID:              %s", ('entity_id', )),
            ("Netmask:                %s", ('netmask', )),
            ("Prefix:                 %s", ("prefix", )),
            ("Description:            '%s'\n"
             "Name-prefix:            '%s'\n"
             "VLAN:                   %s\n"
             "DNS delegated:          %s\n"
             "IP-range:               %s", ("desc", "name_prefix", "vlan",
                                            "delegated", "ip_range")),
            ("Reserved host adresses: %s", ("no_of_res_adr", )),
            ("Reserved addresses:     %s", ("res_adr1", )),
            ("                        %s", ('res_adr', )),
            ("Used addresses:         %s\n"
             "Unused addresses:       %s (excluding reserved adr.)",
             ('used', 'unused')),
        ]))

    def subnet_info(self, operator, identifier):
        """Lists the following information about the given subnet:

        * Subnett
        * Netmask
        * Entity ID
        * Description
        * Name-prefix
        * VLAN number
        * DNS-delegation status
        * Range of IPs on subnet
        * Number of reserved addresses
        * A list of the reserved adresses
        """
        s = self._get_subnet(identifier)
        is_ipv6 = isinstance(s, IPv6Subnet)
        ipc = IPv6Calc if is_ipv6 else IPCalc

        data = {
            'subnet': _subnet_to_identifier(s),
            'entity_id': str(s.entity_id),
            'desc': s.description,
            'delegated': "Yes" if s.dns_delegated else "No",
            'name_prefix': s.name_prefix,
            'no_of_res_adr': str(s.no_of_reserved_adr)
        }

        # ipv4 netmask or ipv6 prefix
        if isinstance(s, Subnet):
            data['netmask'] = ipc.netmask_to_ip(s.subnet_mask)
        else:
            data['prefix'] = '/' + str(s.subnet_mask)

        if s.vlan_number is not None:
            data['vlan'] = str(s.vlan_number)
        else:
            data['vlan'] = "(None)"

        data['ip_range'] = "%s - %s" % (ipc.long_to_ip(
            s.ip_min), ipc.long_to_ip(s.ip_max))

        # Calculate number of used and unused IP-addresses on this subnet
        #                              ^^^^^^ excluding reserved addresses
        uip = self._find.count_used_ips(s.subnet_ip)
        data['used'] = str(uip)
        data['unused'] = str(s.ip_max - s.ip_min - uip - 1)

        reserved_adresses = list(sorted(s.reserved_adr))

        if reserved_adresses:
            data["res_adr1"] = "%s (net)" % ipc.long_to_ip(
                reserved_adresses.pop(0))
        else:
            data["res_adr1"] = "(None)"

        ret = [
            data,
        ]

        if reserved_adresses:
            last_ip = reserved_adresses.pop()
            for address in reserved_adresses:
                ret.append({
                    'res_adr': ipc.long_to_ip(address),
                })
            ret.append({
                'res_adr': "%s (broadcast)" % ipc.long_to_ip(last_ip),
            })

        return ret

    #
    # subnet set_vlan <subnet> <vlan>
    #
    all_commands['subnet_set_vlan'] = Command(
        ("subnet", "set_vlan"),
        SubnetIdentifier(),
        Integer(help_ref="subnet_vlan"),
        fs=FormatSuggestion([
            ("OK; VLAN for subnet %s updated from %i to %i",
             ('subnet_id', 'old_vlan', 'new_vlan')),
        ]),
        perm_filter='is_dns_superuser')

    def subnet_set_vlan(self, operator, identifier, new_vlan):
        self.ba.assert_dns_superuser(operator.get_entity_id())
        try:
            new_vlan = int(new_vlan)
        except:
            raise CerebrumError("VLAN must be an integer; %r isn't" % new_vlan)

        s = self._get_subnet(identifier)
        old_vlan = s.vlan_number
        s.vlan_number = new_vlan
        s.write_db(perform_checks=False)
        return {
            'subnet_id': _subnet_to_identifier(s),
            'old_vlan': old_vlan,
            'new_vlan': new_vlan,
        }

    #
    # subnet set_description <subnet> <description>
    #
    all_commands['subnet_set_description'] = Command(
        ("subnet", "set_description"),
        SubnetIdentifier(),
        SimpleString(help_ref="subnet_description"),
        fs=FormatSuggestion([
            ("OK; description for subnet %s updated to '%s'",
             ('subnet_id', 'new_description')),
        ]))

    def subnet_set_description(self, operator, identifier, new_description):
        self.ba.assert_dns_superuser(operator.get_entity_id())

        s = self._get_subnet(identifier)
        old_description = s.description or ''
        s.description = new_description
        s.write_db(perform_checks=False)
        subnet_id = _subnet_to_identifier(s)
        return {
            'subnet_id': subnet_id,
            'old_description': old_description,
            'new_description': new_description,
        }

    #
    # subnet set_name_prefix
    #
    all_commands['subnet_set_name_prefix'] = Command(
        ("subnet", "set_name_prefix"),
        SubnetIdentifier(),
        SimpleString(help_ref="subnet_name_prefix"),
        fs=FormatSuggestion([
            ("OK; name_prefix for subnet %s updated from '%s' to '%s'",
             ('subnet_id', 'old_prefix', 'new_prefix'))
        ]),
        perm_filter='is_dns_superuser')

    def subnet_set_name_prefix(self, operator, identifier, new_prefix):
        self.ba.assert_dns_superuser(operator.get_entity_id())
        s = self._get_subnet(identifier)
        old_prefix = s.name_prefix
        s.name_prefix = new_prefix
        s.write_db(perform_checks=False)
        subnet_id = self._subnet_to_identifier(s)
        return {
            'subnet_id': subnet_id,
            'old_prefix': old_prefix,
            'new_prefix': new_prefix,
        }

    #
    # subnet set_dns_delegated
    #
    all_commands['subnet_set_dns_delegated'] = Command(
        ("subnet", "set_dns_delegated"),
        SubnetIdentifier(),
        Force(optional=True),
        fs=FormatSuggestion([
            ("Subnet %s set as delegated to external"
             " DNS server", ('subnet_id', )),
            ("Note: %s", ('warning', )),
        ]),
        perm_filter='is_dns_superuser')

    def subnet_set_dns_delegated(self, operator, identifier, force=False):
        self.ba.assert_dns_superuser(operator.get_entity_id())

        s = self._get_subnet(identifier)
        subnet_id = _subnet_to_identifier(s)

        if s.dns_delegated:
            raise CerebrumError("Subnet %s is already set as being delegated"
                                " to external DNS server" % subnet_id)

        ret = [{
            'subnet_id': subnet_id,
        }]

        if s.has_adresses_in_use():
            if not force:
                raise CerebrumError("Subnet '%s' has addresses in use;"
                                    " must force to delegate" % subnet_id)
            ret.append({'warning': "Subnet has address in use!"})

        s.dns_delegated = True
        s.write_db(perform_checks=False)
        return ret

    #
    # subnet unset_dns_delegated
    #
    all_commands['subnet_unset_dns_delegated'] = Command(
        ("subnet", "unset_dns_delegated"),
        SubnetIdentifier(),
        fs=FormatSuggestion([
            ("Subnet %s no longer set as delegated to external"
             " DNS server", ('subnet_id', )),
        ]),
        perm_filter='is_dns_superuser')

    def subnet_unset_dns_delegated(self, operator, identifier):
        self.ba.assert_dns_superuser(operator.get_entity_id())
        s = self._get_subnet(identifier)
        subnet_id = _subnet_to_identifier(s)

        if not s.dns_delegated:
            raise CerebrumError("Subnet %s is already set as not being"
                                " delegated to external DNS server" %
                                subnet_id)

        s.dns_delegated = False
        s.write_db(perform_checks=False)
        return {
            'subnet_id': subnet_id,
        }

    #
    # subnet set_reserved
    #
    all_commands['subnet_set_reserved'] = Command(
        ("subnet", "set_reserved"),
        SubnetIdentifier(),
        Integer(help_ref="subnet_reserved"),
        fs=FormatSuggestion([
            ("OK; Number of reserved addresses for subnet %s "
             "updated from %i to %i", ('subnet_id', 'old_reserved',
                                       'new_reserved')),
            ("FIY: %s", ('warning', )),
        ]),
        perm_filter='is_dns_superuser')

    def subnet_set_reserved(self, operator, identifier, new_res):
        self.ba.assert_dns_superuser(operator.get_entity_id())
        try:
            new_res = int(new_res)
        except:
            raise CerebrumError("The number of reserved addresses must be "
                                "an integer; %r isn't" % new_res)

        if new_res < 0:
            raise CerebrumError("Cannot set number of reserved addresses to " +
                                "a negative number such as '%s'" % new_res)

        s = self._get_subnet(identifier)

        old_res = s.no_of_reserved_adr

        s.no_of_reserved_adr = new_res
        s.calculate_reserved_addresses()

        res = [{
            'subnet_id': _subnet_to_identifier(s),
            'old_reserved': old_res,
            'new_reserved': new_res,
        }]

        if new_res > old_res:
            try:
                s.check_reserved_addresses_in_use()
            except SubnetError as se:
                res.append({
                    'warning': text_type(se),
                })

        s.write_db(perform_checks=False)
        return res
示例#6
0
class BofhdAccessCommands(BofhdCommonMethods):
    """Bofhd extension with access commands"""

    all_commands = {}
    hidden_commands = {}
    authz = BofhdAccessAuth

    @classmethod
    def get_help_strings(cls):
        return merge_help_strings(
            super(BofhdAccessCommands, cls).get_help_strings(),
            (HELP_ACCESS_GROUP, HELP_ACCESS_CMDS, HELP_ACCESS_ARGS))

    #
    # access disk <path>
    #
    all_commands['access_disk'] = Command(
        ('access', 'disk'),
        DiskId(),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_disk(self, operator, path):
        disk = self._get_disk(path)[0]
        result = []
        host = Utils.Factory.get('Host')(self.db)
        try:
            host.find(disk.host_id)
            for r in self._list_access("host", host.name, empty_result=[]):
                if r['attr'] == '' or re.search("/%s$" % r['attr'], path):
                    result.append(r)
        except Errors.NotFoundError:
            pass
        result.extend(self._list_access("disk", path, empty_result=[]))
        return result or "None"

    #
    # access group <group>
    #
    all_commands['access_group'] = Command(
        ('access', 'group'),
        GroupName(help_ref='group_name_id'),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_group(self, operator, group):
        return self._list_access("group", group)

    #
    # access host <hostname>
    #
    all_commands['access_host'] = Command(
        ('access', 'host'),
        SimpleString(help_ref="string_host"),
        fs=FormatSuggestion("%-16s %-16s %-9s %s",
                            ("opset", "attr", "type", "name"),
                            hdr="%-16s %-16s %-9s %s" %
                            ("Operation set", "Pattern", "Type", "Name")))

    def access_host(self, operator, host):
        return self._list_access("host", host)

    #
    # access maildom <maildom>
    #
    all_commands['access_maildom'] = Command(
        ('access', 'maildom'),
        SimpleString(help_ref="email_domain"),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_maildom(self, operator, maildom):
        # TODO: Is this an email command? Should it be moved to bofhd_email?
        return self._list_access("maildom", maildom)

    #
    # access ou <ou>
    #
    all_commands['access_ou'] = Command(
        ('access', 'ou'),
        OU(),
        fs=FormatSuggestion("%-16s %-16s %-9s %s",
                            ("opset", "attr", "type", "name"),
                            hdr="%-16s %-16s %-9s %s" %
                            ("Operation set", "Affiliation", "Type", "Name")))

    def access_ou(self, operator, ou):
        return self._list_access("ou", ou)

    #
    # access user <account>
    #
    all_commands['access_user'] = Command(
        ('access', 'user'),
        AccountName(),
        fs=FormatSuggestion(
            "%-14s %-5s %-20s %-7s %-9s %s",
            ("opset", "target_type", "target", "attr", "type", "name"),
            hdr="%-14s %-5s %-20s %-7s %-9s %s" %
            ("Operation set", "TType", "Target", "Attr", "Type", "Name")))

    def access_user(self, operator, user):
        # This is more tricky than the others, we want to show anyone with
        # access, through OU, host or disk.  (not global_XXX, though.)
        #
        # Note that there is no auth-type 'account', so you can't be granted
        # direct access to a specific user.

        acc = self._get_account(user)
        # Make lists of the disks and hosts associated with the user
        disks = {}
        hosts = {}
        disk = Utils.Factory.get("Disk")(self.db)
        for r in acc.get_homes():
            # Disk for archived users may not exist anymore
            try:
                disk_id = int(r['disk_id'])
            except TypeError:
                continue
            if disk_id not in disks:
                disk.clear()
                disk.find(disk_id)
                disks[disk_id] = disk.path
                if disk.host_id is not None:
                    basename = disk.path.split("/")[-1]
                    host_id = int(disk.host_id)
                    if host_id not in hosts:
                        hosts[host_id] = []
                    hosts[host_id].append(basename)
        # Look through disks
        ret = []
        for d in disks.keys():
            for entry in self._list_access("disk", d, empty_result=[]):
                entry['target_type'] = "disk"
                entry['target'] = disks[d]
                ret.append(entry)
        # Look through hosts:
        for h in hosts.keys():
            for candidate in self._list_access("host", h, empty_result=[]):
                candidate['target_type'] = "host"
                candidate['target'] = self._get_host(h).name
                if candidate['attr'] == "":
                    ret.append(candidate)
                    continue
                for dir in hosts[h]:
                    if re.match(candidate['attr'], dir):
                        ret.append(candidate)
                        break
        # TODO: check user's ou(s)
        ret.sort(lambda x, y: (cmp(x['opset'].lower(), y['opset'].lower()) or
                               cmp(x['name'], y['name'])))
        return ret

    #
    # access global_group
    #
    all_commands['access_global_group'] = Command(
        ('access', 'global_group'),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_global_group(self, operator):
        return self._list_access("global_group")

    #
    # access global_host
    #
    all_commands['access_global_host'] = Command(
        ('access', 'global_host'),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_global_host(self, operator):
        return self._list_access("global_host")

    #
    # access global_maildom
    #
    all_commands['access_global_maildom'] = Command(
        ('access', 'global_maildom'),
        fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"),
                            hdr="%-16s %-9s %s" %
                            ("Operation set", "Type", "Name")))

    def access_global_maildom(self, operator):
        return self._list_access("global_maildom")

    #
    # access global_ou
    #
    all_commands['access_global_ou'] = Command(
        ('access', 'global_ou'),
        fs=FormatSuggestion("%-16s %-16s %-9s %s",
                            ("opset", "attr", "type", "name"),
                            hdr="%-16s %-16s %-9s %s" %
                            ("Operation set", "Affiliation", "Type", "Name")))

    def access_global_ou(self, operator):
        return self._list_access("global_ou")

    #
    # access global_dns
    #
    all_commands['access_global_dns'] = Command(
        ('access', 'global_dns'),
        fs=FormatSuggestion("%-16s %-16s %-9s %s",
                            ("opset", "attr", "type", "name"),
                            hdr="%-16s %-16s %-9s %s" %
                            ("Operation set", "Affiliation", "Type", "Name")))

    def access_global_dns(self, operator):
        return self._list_access("global_dns")

    # TODO: Define all_commands['access_global_dns']
    def access_global_person(self, operator):
        return self._list_access("global_person")

    #
    # access grant <opset name> <who> <type> <on what> [<attr>]
    #
    all_commands['access_grant'] = Command(
        ('access', 'grant'),
        OpSet(),
        GroupName(help_ref="id:target:group"),
        EntityType(default='group', help_ref="auth_entity_type"),
        SimpleString(optional=True, help_ref="auth_target_entity"),
        SimpleString(optional=True, help_ref="auth_attribute"),
        perm_filter='can_grant_access')

    def access_grant(self,
                     operator,
                     opset,
                     group,
                     entity_type,
                     target_name=None,
                     attr=None):
        return self._manipulate_access(self._grant_auth, operator, opset,
                                       group, entity_type, target_name, attr)

    #
    # access revoke <opset name> <who> <type> <on what> [<attr>]
    #
    all_commands['access_revoke'] = Command(
        ('access', 'revoke'),
        OpSet(),
        GroupName(help_ref="id:target:group"),
        EntityType(default='group', help_ref="auth_entity_type"),
        SimpleString(help_ref="auth_target_entity"),
        SimpleString(optional=True, help_ref="auth_attribute"),
        perm_filter='can_grant_access')

    def access_revoke(self,
                      operator,
                      opset,
                      group,
                      entity_type,
                      target_name,
                      attr=None):
        return self._manipulate_access(self._revoke_auth, operator, opset,
                                       group, entity_type, target_name, attr)

    #
    # access list_opsets
    #
    all_commands['access_list_opsets'] = Command(
        ('access', 'list_opsets'),
        fs=FormatSuggestion("%s", ("opset", ), hdr="Operation set"))

    def access_list_opsets(self, operator):
        baos = BofhdAuthOpSet(self.db)
        ret = []
        for r in baos.list():
            ret.append({'opset': r['name']})
        ret.sort(lambda x, y: cmp(x['opset'].lower(), y['opset'].lower()))
        return ret

    #
    # access list_alterable [group] [username]
    #
    hidden_commands['access_list_alterable'] = Command(
        ('access', 'list_alterable'),
        SimpleString(optional=True),
        AccountName(optional=True),
        fs=FormatSuggestion("%s %s", ("entity_name", "description")))

    def access_list_alterable(self,
                              operator,
                              target_type='group',
                              access_holder=None):
        """List entities that access_holder can moderate."""

        if access_holder is None:
            account_id = operator.get_entity_id()
        else:
            account = self._get_account(access_holder, actype="PosixUser")
            account_id = account.entity_id

        if not (account_id == operator.get_entity_id()
                or self.ba.is_superuser(operator.get_entity_id())):
            raise PermissionDenied("You do not have permission for this"
                                   " operation")
        result = []
        matches = self.Group_class(self.db).search(admin_id=account_id,
                                                   admin_by_membership=True)
        matches += self.Group_class(self.db).search(
            moderator_id=account_id, moderator_by_membership=True)
        if len(matches) > cereconf.BOFHD_MAX_MATCHES_ACCESS:
            raise CerebrumError("More than {:d} ({:d}) matches. Refusing to "
                                "return result".format(
                                    cereconf.BOFHD_MAX_MATCHES_ACCESS,
                                    len(matches)))
        for row in matches:
            try:
                group = self._get_group(row['group_id'])
            except Errors.NotFoundError:
                self.logger.warn(
                    "Non-existent entity (%s) referenced from auth_op_target",
                    row["entity_id"])
                continue
            tmp = {
                "entity_name": group.group_name,
                "description": group.description,
                "expire_date": group.expire_date,
            }
            if tmp not in result:
                result.append(tmp)
        return result

    #
    # access show_opset <opset name>
    #
    all_commands['access_show_opset'] = Command(
        ('access', 'show_opset'),
        OpSet(),
        fs=FormatSuggestion("%-16s %-16s %s", ("op", "attr", "desc"),
                            hdr="%-16s %-16s %s" %
                            ("Operation", "Attribute", "Description")))

    def access_show_opset(self, operator, opset=None):
        baos = BofhdAuthOpSet(self.db)
        try:
            baos.find_by_name(opset)
        except Errors.NotFoundError:
            raise CerebrumError("Unknown operation set: '{}'".format(opset))
        ret = []
        for r in baos.list_operations():
            entry = {
                'op': six.text_type(self.const.AuthRoleOp(r['op_code'])),
                'desc': self.const.AuthRoleOp(r['op_code']).description,
            }
            attrs = []
            for r2 in baos.list_operation_attrs(r['op_id']):
                attrs += [r2['attr']]
            if not attrs:
                attrs = [""]
            for a in attrs:
                entry_with_attr = entry.copy()
                entry_with_attr['attr'] = a
                ret += [entry_with_attr]
        ret.sort(lambda x, y:
                 (cmp(x['op'], y['op']) or cmp(x['attr'], y['attr'])))
        return ret

    # TODO
    #
    # To be able to manipulate all aspects of bofhd authentication, we
    # need a few more commands:
    #
    #   access create_opset <opset name>
    #   access create_op <opname> <desc>
    #   access delete_op <opname>
    #   access add_to_opset <opset> <op> [<attr>]
    #   access remove_from_opset <opset> <op> [<attr>]
    #
    # The opset could be implicitly deleted after the last op was
    # removed from it.

    #
    # access list <owner> [target_type]
    #
    all_commands['access_list'] = Command(
        ('access', 'list'),
        SimpleString(help_ref='id:target:group'),
        SimpleString(help_ref='string_perm_target_type_access', optional=True),
        fs=FormatSuggestion(
            "%-14s %-16s %-30s %-7s",
            ("opset", "target_type", "target", "attr"),
            hdr="%-14s %-16s %-30s %-7s" %
            ("Operation set", "Target type", "Target", "Attr")))

    def access_list(self, operator, owner, target_type=None):
        """
        List everything an account or group can operate on. Only direct
        ownership is reported: the entities an account can access due to group
        memberships will not be listed. This does not include unpersonal users
        owned by groups.

        :param operator: operator in bofh session
        :param owner: str name of owner object
        :param target_type: the type of the target
        :return: List of everything an account or group can operate on
        """

        ar = BofhdAuthRole(self.db)
        aot = BofhdAuthOpTarget(self.db)
        aos = BofhdAuthOpSet(self.db)
        co = self.const
        owner_id = self.util.get_target(owner,
                                        default_lookup="group",
                                        restrict_to=[]).entity_id
        ret = []
        for role in ar.list(owner_id):
            aos.clear()
            aos.find(role['op_set_id'])
            for r in aot.list(target_id=role['op_target_id']):
                if target_type is not None and r['target_type'] != target_type:
                    continue
                if r['entity_id'] is None:
                    target_name = "N/A"
                elif r['target_type'] == co.auth_target_type_maildomain:
                    # FIXME: EmailDomain is not an Entity.
                    ed = Email.EmailDomain(self.db)
                    try:
                        ed.find(r['entity_id'])
                    except (Errors.NotFoundError, ValueError):
                        self.logger.warn("Non-existing entity (e-mail domain) "
                                         "in auth_op_target {}:{:d}".format(
                                             r['target_type'], r['entity_id']))
                        continue
                    target_name = ed.email_domain_name
                elif r['target_type'] == co.auth_target_type_ou:
                    ou = self.OU_class(self.db)
                    try:
                        ou.find(r['entity_id'])
                    except (Errors.NotFoundError, ValueError):
                        self.logger.warn("Non-existing entity (ou) in "
                                         "auth_op_target %s:%d" %
                                         (r['target_type'], r['entity_id']))
                        continue
                    target_name = "%02d%02d%02d (%s)" % (
                        ou.fakultet, ou.institutt, ou.avdeling, ou.short_name)
                elif r['target_type'] == co.auth_target_type_dns:
                    s = Subnet(self.db)
                    # TODO: should Subnet.find() support ints as input?
                    try:
                        s.find('entity_id:%s' % r['entity_id'])
                    except (Errors.NotFoundError, ValueError, SubnetError):
                        self.logger.warn("Non-existing entity (subnet) in "
                                         "auth_op_target %s:%d" %
                                         (r['target_type'], r['entity_id']))
                        continue
                    target_name = "%s/%s" % (s.subnet_ip, s.subnet_mask)
                else:
                    try:
                        ety = self._get_entity(ident=r['entity_id'])
                        target_name = self._get_name_from_object(ety)
                    except (Errors.NotFoundError, ValueError):
                        self.logger.warn("Non-existing entity in "
                                         "auth_op_target %s:%d" %
                                         (r['target_type'], r['entity_id']))
                        continue
                ret.append({
                    'opset': aos.name,
                    'target_type': r['target_type'],
                    'target': target_name,
                    'attr': r['attr'] or "",
                })
        ret.sort(lambda a, b: (cmp(a['target_type'], b['target_type']) or cmp(
            a['target'], b['target'])))
        return ret

    # access dns <dns-target>
    all_commands['access_dns'] = Command(
        ('access', 'dns'),
        SimpleString(),
        fs=FormatSuggestion("%-16s %-9s %-9s %s",
                            ("opset", "type", "level", "name"),
                            hdr="%-16s %-9s %-9s %s" %
                            ("Operation set", "Type", "Level", "Name")))

    def access_dns(self, operator, dns_target):
        ret = []
        if '/' in dns_target:
            # Asking for rights on subnet; IP not of interest
            for accessor in self._list_access("dns",
                                              dns_target,
                                              empty_result=[]):
                accessor["level"] = "Subnet"
                ret.append(accessor)
        else:
            # Asking for rights on IP; need to provide info about
            # rights on the IP's subnet too
            for accessor in self._list_access("dns",
                                              dns_target + '/',
                                              empty_result=[]):
                accessor["level"] = "Subnet"
                ret.append(accessor)
            for accessor in self._list_access("dns",
                                              dns_target,
                                              empty_result=[]):
                accessor["level"] = "IP"
                ret.append(accessor)
        return ret

    #
    # Helper methods
    #

    def _list_access(self, target_type, target_name=None, empty_result="None"):
        target_id, target_type, target_auth = self._get_access_id(
            target_type, target_name)
        ret = []
        ar = BofhdAuthRole(self.db)
        aos = BofhdAuthOpSet(self.db)
        for r in self._get_auth_op_target(target_id,
                                          target_type,
                                          any_attr=True):
            attr = str(r['attr'] or '')
            for r2 in ar.list(op_target_id=r['op_target_id']):
                aos.clear()
                aos.find(r2['op_set_id'])
                ety = self._get_entity(ident=r2['entity_id'])
                ret.append({
                    'opset':
                    aos.name,
                    'attr':
                    attr,
                    'type':
                    six.text_type(self.const.EntityType(ety.entity_type)),
                    'name':
                    self._get_name_from_object(ety),
                })
        ret.sort(lambda a, b:
                 (cmp(a['opset'], b['opset']) or cmp(a['name'], b['name'])))
        return ret or empty_result

    def _manipulate_access(self, change_func, operator, opset, group,
                           entity_type, target_name, attr):
        """This function does no validation of types itself.  It uses
        _get_access_id() to get a (target_type, entity_id) suitable for
        insertion in auth_op_target.  Additional checking for validity
        is done by _validate_access().

        Those helper functions look for a function matching the
        target_type, and call it.  There should be one
        _get_access_id_XXX and one _validate_access_XXX for each known
        target_type.

        """
        opset = self._get_opset(opset)
        gr = self.util.get_target(group,
                                  default_lookup="group",
                                  restrict_to=['Account', 'Group'])
        target_id, target_type, target_auth = self._get_access_id(
            entity_type, target_name)
        operator_id = operator.get_entity_id()
        if target_auth is None and not self.ba.is_superuser(operator_id):
            raise PermissionDenied("Currently limited to superusers")
        else:
            self.ba.can_grant_access(operator_id, target_auth, target_type,
                                     target_id, opset)
        self._validate_access(entity_type, opset, attr)
        return change_func(gr.entity_id, opset, target_id, target_type, attr,
                           group, target_name)

    def _get_access_id(self, target_type, target_name):
        """Get required data for granting access to an operation target.

        :param str target_type: The type of

        :rtype: tuple
        :returns:
            A three element tuple with information about the operation target:

              1. The entity_id of the target entity (int)
              2. The target type (str)
              3. The `intval` of the operation constant for granting access to
                 the given target entity.

        """
        lookup = LookupClass()
        if target_type in lookup:
            lookupclass = lookup[target_type](self.db)
            return lookupclass.get(target_name)
        else:
            raise CerebrumError("Unknown id type {}".format(target_type))

    def _validate_access(self, target_type, opset, attr):
        lookup = LookupClass()
        if target_type in lookup:
            lookupclass = lookup[target_type](self.db)
            return lookupclass.validate(opset, attr)
        else:
            raise CerebrumError("Unknown type %s" % target_type)

    def _revoke_auth(self, entity_id, opset, target_id, target_type, attr,
                     entity_name, target_name):
        op_target_id = self._get_auth_op_target(target_id, target_type, attr)
        if not op_target_id:
            raise CerebrumError(
                "No one has matching access to {}".format(target_name))
        ar = BofhdAuthRole(self.db)
        rows = ar.list(entity_id, opset.op_set_id, op_target_id)
        if len(rows) == 0:
            return "%s doesn't have %s access to %s %s" % (
                entity_name, opset.name, six.text_type(target_type),
                target_name)
        ar.revoke_auth(entity_id, opset.op_set_id, op_target_id)
        # See if the op_target has any references left, delete it if not.
        rows = ar.list(op_target_id=op_target_id)
        if len(rows) == 0:
            aot = BofhdAuthOpTarget(self.db)
            aot.find(op_target_id)
            aot.delete()
        return "OK, revoked %s access for %s from %s %s" % (
            opset.name, entity_name, six.text_type(target_type), target_name)

    def _grant_auth(self, entity_id, opset, target_id, target_type, attr,
                    entity_name, target_name):
        op_target_id = self._get_auth_op_target(target_id,
                                                target_type,
                                                attr,
                                                create=True)
        ar = BofhdAuthRole(self.db)
        rows = ar.list(entity_id, opset.op_set_id, op_target_id)
        if len(rows) == 0:
            ar.grant_auth(entity_id, opset.op_set_id, op_target_id)
            return "OK, granted %s access %s to %s %s" % (
                entity_name, opset.name, six.text_type(target_type),
                target_name)
        return "%s already has %s access to %s %s" % (
            entity_name, opset.name, six.text_type(target_type), target_name)
示例#7
0
class BofhdExtension(BofhdCommonMethods):
    u""" Commands for managing Feide services and multifactor
    authentication. """

    hidden_commands = {}  # Not accessible through bofh
    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    def _find_service(self, service_name):
        fse = FeideService(self.db)
        try:
            fse.find_by_name(service_name)
        except Errors.NotFoundError:
            raise CerebrumError('No such Feide service')
        return fse

    @classmethod
    def get_help_strings(cls):
        u""" Help strings for Feide commands. """
        group_help = {
            'feide': 'Commands for Feide multifactor authentication',
        }

        command_help = {
            'feide': {
                'feide_service_add': cls.feide_service_add.__doc__,
                'feide_service_remove': cls.feide_service_remove.__doc__,
                'feide_service_list': cls.feide_service_list.__doc__,
                'feide_authn_level_add': cls.feide_authn_level_add.__doc__,
                'feide_authn_level_remove':
                cls.feide_authn_level_remove.__doc__,
                'feide_authn_level_list': cls.feide_authn_level_list.__doc__,
            },
        }

        arg_help = {
            'feide_service_name': ['service_name', 'Enter Feide service name'],
            'feide_service_id': ['feide_id', 'Enter Feide service ID'],
            'feide_service_confirm_remove': [
                'confirm',
                'This will remove any authentication levels associated with '
                'this service. Continue? [y/n]'
            ],
            'feide_authn_level': ['level', 'Enter authentication level'],
            'feide_authn_entity_target': [
                'entity', 'Enter an existing entity',
                """Enter the entity as type:name, for example: 'group:admin-users'

                 If only a name is entered, it will be assumed to be an
                 account name.

                 Supported types are:
                  - 'person' (name of user => Person)
                  - 'group' (name of group => Group)
                  - 'id' (entity ID => any Person or Group)
                 """
            ],
        }

        return (group_help, command_help, arg_help)

    # feide service_add
    all_commands['feide_service_add'] = Command(
        ('feide', 'service_add'),
        Integer(help_ref='feide_service_id'),
        SimpleString(help_ref='feide_service_name'),
        perm_filter='is_superuser')

    def feide_service_add(self, operator, feide_id, service_name):
        """ Add a Feide service """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may add Feide services')
        if not feide_id.isdigit():
            raise CerebrumError('Feide ID can only contain digits.')
        fse = FeideService(self.db)
        service_name = service_name.strip()
        name_error = fse.illegal_name(service_name)
        if name_error:
            raise CerebrumError(name_error)
        for service in fse.search():
            if int(feide_id) == int(service['feide_id']):
                raise CerebrumError(
                    'A Feide service with that ID already exists')
            if service_name == service['name']:
                raise CerebrumError(
                    'A Feide service with that name already exists')
        fse.populate(feide_id, service_name)
        fse.write_db()
        return "Added Feide service '{}'".format(service_name)

    # feide service_remove
    all_commands['feide_service_remove'] = Command(
        ('feide', 'service_remove'),
        SimpleString(help_ref='feide_service_name'),
        YesNo(help_ref='feide_service_confirm_remove'),
        perm_filter='is_superuser')

    def feide_service_remove(self, operator, service_name, confirm):
        """ Remove a Feide service. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may remove Feide services')
        if not confirm:
            return 'No action taken.'
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        fse.delete()
        fse.write_db()
        return "Removed Feide service '{}'".format(service_name)

    # feide service_list
    all_commands['feide_service_list'] = Command(
        ('feide', 'service_list'),
        fs=FormatSuggestion(
            '%-12d %-12d %s', ('service_id', 'feide_id', 'name'),
            hdr='%-12s %-12s %s' % ('Entity ID', 'Feide ID', 'Name')),
        perm_filter='is_superuser')

    def feide_service_list(self, operator):
        """ List Feide services. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may list Feide services')
        fse = FeideService(self.db)
        return map(dict, fse.search())

    # feide authn_level_add
    all_commands['feide_authn_level_add'] = Command(
        ('feide', 'authn_level_add'),
        SimpleString(help_ref='feide_service_name'),
        SimpleString(help_ref='feide_authn_entity_target'),
        Integer(help_ref='feide_authn_level'),
        perm_filter='is_superuser')

    def feide_authn_level_add(self, operator, service_name, target, level):
        """ Add an authentication level for a given service and entity. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may add Feide authentication levels')
        if not level.isdigit() or int(level) not in (3, 4):
            raise CerebrumError('Authentication level must be 3 or 4')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        # Allow authentication levels for persons and groups
        entity = self.util.get_target(target,
                                      default_lookup='person',
                                      restrict_to=['Person', 'Group'])
        if entity.search_authn_level(service_id=fse.entity_id,
                                     entity_id=entity.entity_id,
                                     level=level):
            raise CerebrumError(
                'Authentication level {} for {} for service {} '
                'already enabled'.format(level, target, service_name))
        entity.add_authn_level(service_id=fse.entity_id, level=level)
        return 'Added authentication level {} for {} for {}'.format(
            level, target, service_name)

    # feide authn_level_remove
    all_commands['feide_authn_level_remove'] = Command(
        ('feide', 'authn_level_remove'),
        SimpleString(help_ref='feide_service_name'),
        SimpleString(help_ref='feide_authn_entity_target'),
        Integer(help_ref='feide_authn_level'),
        perm_filter='is_superuser')

    def feide_authn_level_remove(self, operator, service_name, target, level):
        """ Remove an authentication level for a given service and entity. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may remove Feide authentication levels')
        if not level.isdigit() or int(level) not in (3, 4):
            raise CerebrumError('Authentication level must be 3 or 4')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        # Allow authentication levels for persons and groups
        entity = self.util.get_target(target,
                                      default_lookup='person',
                                      restrict_to=['Person', 'Group'])
        if not entity.search_authn_level(service_id=fse.entity_id,
                                         entity_id=entity.entity_id,
                                         level=level):
            raise CerebrumError(
                'No such authentication level {} for {} for service {}'.format(
                    level, target, service_name))
        entity.remove_authn_level(service_id=fse.entity_id, level=level)
        return 'Removed authentication level {} for {} for {}'.format(
            level, target, service_name)

    # feide authn_level_search
    all_commands['feide_authn_level_list'] = Command(
        ('feide', 'authn_level_list'),
        SimpleString(help_ref='feide_service_name'),
        fs=FormatSuggestion(
            '%-20s %-6d %s', ('service_name', 'level', 'entity'),
            hdr='%-20s %-6s %s' % ('Service', 'Level', 'Entity')),
        perm_filter='is_superuser')

    def feide_authn_level_list(self, operator, service_name):
        """ List all authentication levels for a service. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may list Feide authentication levels')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        en = Factory.get('Entity')(self.db)
        fsal = FeideServiceAuthnLevelMixin(self.db)
        result = []
        for x in fsal.search_authn_level(service_id=fse.entity_id):
            try:
                en.clear()
                en.find(x['entity_id'])
                entity_type = str(self.const.map_const(en.entity_type))
                entity_name = self._get_entity_name(en.entity_id,
                                                    en.entity_type)
                entity = '{} {} (id:{:d})'.format(entity_type, entity_name,
                                                  en.entity_id)
            except:
                entity = 'id:{}'.format(x['entity_id'])
            result.append({
                'service_name': service_name,
                'level': x['level'],
                'entity': entity
            })
        return result
示例#8
0
class EmailCommands(bofhd_email.BofhdEmailCommands):
    """ UiO specific email commands and overloads. """

    all_commands = {}
    hidden_commands = {}
    omit_parent_commands = {}
    parent_commands = True
    authz = bofhd_auth.EmailAuth

    @classmethod
    def get_help_strings(cls):
        email_cmds = {
            'email': {
                'email_forward_info':
                    "Show information about an address that is forwarded to",
                'email_move':
                    "Move a user's e-mail to another server",
                'email_show_reservation_status':
                    "Show reservation status for an account",
                "email_move_domain_addresses":
                    "Move the first account's e-mail addresses at a domain to "
                    "the second account",
            }
        }
        arg_help = {
            'yes_no_move_primary':
                ['move_primary',
                 'Should primary email address be moved? (y/n)'],
        }
        return merge_help_strings(
            super(EmailCommands, cls).get_help_strings(),
            ({}, email_cmds, arg_help))

    def __email_forward_destination_allowed(self, account, address):
        """ Check if the forward is compilant with Norwegian law"""
        person = Utils.Factory.get('Person')(self.db)
        if (account.owner_type == self.const.entity_person and
                person.list_affiliations(
                    person_id=account.owner_id,
                    source_system=self.const.system_sap,
                    affiliation=self.const.affiliation_ansatt)):
            try:
                self._get_email_domain_from_str(address.split('@')[-1])
            except CerebrumError:
                return False
        return True

    def _get_email_target_and_address(self, address):
        # Support DistributionGroup email target lookup
        try:
            return super(EmailCommands,
                         self)._get_email_target_and_address(address)
        except CerebrumError as e:
            # Not found, maybe distribution group?
            try:
                dlgroup = Utils.Factory.get("DistributionGroup")(self.db)
                dlgroup.find_by_name(address)
                et = Email.EmailTarget(self.db)
                et.find_by_target_entity(dlgroup.entity_id)
                epa = Email.EmailPrimaryAddressTarget(self.db)
                epa.find(et.entity_id)
                ea = Email.EmailAddress(self.db)
                ea.find(epa.email_primaddr_id)
                return et, ea
            except Errors.NotFoundError:
                raise e

    def _get_email_target_and_dlgroup(self, address):
        """Returns a tuple consisting of the email target associated
        with address and the account if the target type is user.  If
        there is no at-sign in address, assume it is an account name.
        Raises CerebrumError if address is unknown."""
        et, ea = self._get_email_target_and_address(address)
        grp = None
        # what will happen if the target was a dl_group but is now
        # deleted? it's possible that we should have created a new
        # target_type = dlgroup_deleted, but it seemed redundant earlier
        # now, i'm not so sure (Jazz, 2013-12(
        if et.email_target_type in (self.const.email_target_dl_group,
                                    self.const.email_target_deleted):
            grp = self._get_group(et.email_target_entity_id,
                                  idtype='id',
                                  grtype="DistributionGroup")
        return et, grp

    def _is_email_delivery_stopped(self, ldap_target):
        """ Test if email delivery is turned off in LDAP for a user. """
        import ldap
        import ldap.filter
        import ldap.ldapobject
        ldapconns = [ldap.ldapobject.ReconnectLDAPObject("ldap://%s/" % server)
                     for server in cereconf.LDAP_SERVERS]
        target_filter = ("(&(target=%s)(mailPause=TRUE))" %
                         ldap.filter.escape_filter_chars(ldap_target))
        for conn in ldapconns:
            try:
                # FIXME: cereconf.LDAP_MAIL['dn'] has a bogus value, so we
                # must hardcode the DN.
                res = conn.search_s("cn=targets,cn=mail,dc=uio,dc=no",
                                    ldap.SCOPE_ONELEVEL, target_filter,
                                    ["1.1"])
                if len(res) != 1:
                    return False
            except ldap.LDAPError:
                self.logger.error("LDAP search failed", exc_info=True)
                return False
        return True

    def _email_info_detail(self, acc):
        info = []
        eq = Email.EmailQuota(self.db)
        try:
            eq.find_by_target_entity(acc.entity_id)
            et = Email.EmailTarget(self.db)
            et.find_by_target_entity(acc.entity_id)
            es = Email.EmailServer(self.db)
            es.find(et.email_server_id)

            # exchange-relatert-jazz
            # since Exchange-users will have a different kind of
            # server this code will not be affected at Exchange
            # roll-out It may, however, be removed as soon as
            # migration is completed (up to and including
            # "dis_quota_soft': eq.email_quota_soft})")
            if es.email_server_type == self.const.email_server_type_cyrus:
                pw = self.db._read_password(cereconf.CYRUS_HOST,
                                            cereconf.CYRUS_ADMIN)
                used = 'N/A'
                limit = None
                try:
                    cyrus = Utils.CerebrumIMAP4_SSL(
                        es.name,
                        ssl_version=ssl.PROTOCOL_TLSv1)
                    # IVR 2007-08-29 If the server is too busy, we do not want
                    # to lock the entire bofhd.
                    # 5 seconds should be enough
                    cyrus.socket().settimeout(5)
                    cyrus.login(cereconf.CYRUS_ADMIN, pw)
                    res, quotas = cyrus.getquota("user." + acc.account_name)
                    cyrus.socket().settimeout(None)
                    if res == "OK":
                        for line in quotas:
                            try:
                                folder, qtype, qused, qlimit = line.split()
                                if qtype == "(STORAGE":
                                    used = str(int(qused)/1024)
                                    limit = int(qlimit.rstrip(")"))/1024
                            except ValueError:
                                # line.split fails e.g. because quota isn't set
                                # on server
                                folder, junk = line.split()
                                self.logger.warning("No IMAP quota set for %r",
                                                    acc.account_name)
                                used = "N/A"
                                limit = None
                except (bofhd_uio_cmds.TimeoutException, socket.error):
                    used = 'DOWN'
                except bofhd_uio_cmds.ConnectException as e:
                    used = exc_to_text(e)
                except imaplib.IMAP4.error:
                    used = 'DOWN'
                info.append({'quota_hard': eq.email_quota_hard,
                             'quota_soft': eq.email_quota_soft,
                             'quota_used': used})
                if limit is not None and limit != eq.email_quota_hard:
                    info.append({'quota_server': limit})
            else:
                info.append({'dis_quota_hard': eq.email_quota_hard,
                             'dis_quota_soft': eq.email_quota_soft})
        except Errors.NotFoundError:
            pass
        # exchange-relatert-jazz
        # delivery for exchange-mailboxes is not regulated through
        # LDAP, and LDAP should not be checked there my be some need
        # to implement support for checking if delivery is paused in
        # Exchange, but at this point only very vague explanation has
        # been given and priority is therefore low
        if acc.has_spread(self.const.spread_uit_exchange):
            return info
        # Check if the ldapservers have set mailPaused
        if self._is_email_delivery_stopped(acc.account_name):
            info.append({'status': 'Paused (migrating to new server)'})

        return info

    def _email_info_dlgroup(self, groupname):
        et, dl_group = self._get_email_target_and_dlgroup(groupname)
        ret = []
        # we need to make the return value conform with the
        # client requeirements
        tmpret = dl_group.get_distgroup_attributes_and_targetdata()
        for x in tmpret:
            if tmpret[x] == 'T':
                ret.append({x: 'Yes'})
                continue
            elif tmpret[x] == 'F':
                ret.append({x: 'No'})
                continue
            ret.append({x: tmpret[x]})
        return ret

    #
    # email forward_add <account>+ <address>+
    #
    def email_forward_add(self, operator, uname, address):
        """Add an email-forward to a email-target asociated with an account."""
        # Override email_forward_add with check for employee email addr
        et, acc = self._get_email_target_and_account(uname)
        if acc and not self.__email_forward_destination_allowed(acc, address):
            raise CerebrumError("Employees cannot forward e-mail to"
                                " external addresses")
        return super(EmailCommands, self).email_forward_add(operator,
                                                            uname,
                                                            address)

    #
    # email forward_info
    #
    all_commands['email_forward_info'] = Command(
        ('email', 'forward_info'),
        EmailAddress(),
        fs=FormatSuggestion([('%s', ('id', ))]),
        perm_filter='can_email_forward_info',
    )

    def email_forward_info(self, operator, forward_to):
        """List owners of email forwards."""
        self.ba.can_email_forward_info(operator.get_entity_id())
        ef = Email.EmailForward(self.db)
        et = Email.EmailTarget(self.db)
        ac = Utils.Factory.get('Account')(self.db)
        ret = []

        # Different output format for different input.
        def rfun(r):
            return (r if '%' not in forward_to
                    else '%-12s %s' % (r, fwd['forward_to']))

        for fwd in ef.search(forward_to):
            try:
                et.clear()
                ac.clear()
                et.find(fwd['target_id'])
                ac.find(et.email_target_entity_id)
                ret.append({'id': rfun(ac.account_name)})
            except Errors.NotFoundError:
                ret.append({'id': rfun('id:%s' % et.entity_id)})
        return ret

    #
    # email show_reservation_status
    #
    all_commands['email_show_reservation_status'] = Command(
        ('email', 'show_reservation_status'),
        AccountName(),
        fs=FormatSuggestion([("%-9s %s", ("uname", "hide"))]),
        perm_filter='is_postmaster')

    def email_show_reservation_status(self, operator, uname):
        """Display reservation status for a person."""
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('Access to this command is restricted')
        hidden = True
        account = self._get_account(uname)
        if account.owner_type == self.const.entity_person:
            person = self._get_person('entity_id', account.owner_id)
            if person.has_e_reservation():
                hidden = True
            elif person.get_primary_account() != account.entity_id:
                hidden = True
            else:
                hidden = False
        return {
            'uname': uname,
            'hide': 'hidden' if hidden else 'visible',
        }

    #
    # email move
    #
    all_commands['email_move'] = Command(
        ("email", "move"),
        AccountName(help_ref="account_name", repeat=True),
        SimpleString(help_ref='string_email_host'),
        perm_filter='can_email_move')

    def email_move(self, operator, uname, server):
        acc = self._get_account(uname)
        self.ba.can_email_move(operator.get_entity_id(), acc)
        et = Email.EmailTarget(self.db)
        et.find_by_target_entity(acc.entity_id)
        old_server = et.email_server_id
        es = Email.EmailServer(self.db)
        try:
            es.find_by_name(server)
        except Errors.NotFoundError:
            raise CerebrumError("%r is not registered as an e-mail server" %
                                server)
        if old_server == es.entity_id:
            raise CerebrumError("User is already at %s" % server)

        et.email_server_id = es.entity_id
        et.write_db()
        return "OK, updated e-mail server for %s (to %s)" % (uname, server)

    #
    # email move_domain_addresses
    #
    all_commands['email_move_domain_addresses'] = Command(
        ("email", "move_domain_addresses"),
        AccountName(help_ref="account_name"),
        AccountName(help_ref="account_name"),
        SimpleString(help_ref='email_domain', optional=True,
                     default=cereconf.NO_MAILBOX_DOMAIN_EMPLOYEES),
        YesNo(help_ref='yes_no_move_primary', optional=True, default="No"),
        perm_filter="is_superuser")

    def _move_email_address(self, address, reassigned_addresses, dest_et):
        ea = Email.EmailAddress(self.db)
        ea.find(address['address_id'])
        ea.email_addr_target_id = dest_et.entity_id
        ea.write_db()
        reassigned_addresses.append(ea.get_address())

    def _move_primary_email_address(self, address, reassigned_addresses,
                                    dest_et, epat):
        epat.delete()
        self._move_email_address(address, reassigned_addresses, dest_et)
        epat.clear()
        try:
            epat.find(dest_et.entity_id)
        except Errors.NotFoundError:
            pass
        else:
            epat.delete()
        epat.clear()
        epat.populate(address['address_id'], parent=dest_et)
        epat.write_db()

    def _move_ad_email(self, email, dest_uname):
        ad = ad_email.AdEmail(self.db)
        ad.delete_ad_email(account_name=dest_uname)
        ad.set_ad_email(dest_uname, email['local_part'], email['domain_part'])

        ad_emails_added = "Updated ad email {} for {}. ".format(
            email['local_part']+"@"+email['domain_part'],
            dest_uname
        )
        return ad_emails_added

    def email_move_domain_addresses(self, operator, source_uname, dest_uname,
                                    domain_str, move_primary):
        """Move an account's e-mail addresses to another account

        :param domain_str: email domain to be affected
        :param move_primary: move primary email address
        """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Currently limited to superusers")

        move_primary = self._get_boolean(move_primary)
        source_account = self._get_account(source_uname)
        source_et = self._get_email_target_for_account(source_account)
        dest_account = self._get_account(dest_uname)
        dest_et = self._get_email_target_for_account(dest_account)
        epat = Email.EmailPrimaryAddressTarget(self.db)

        try:
            epat.find(source_et.entity_id)
        except Errors.NotFoundError:
            epat.clear()

        reassigned_addresses = []
        for address in source_et.get_addresses():
            if address['domain'] == domain_str:

                if address['address_id'] == epat.email_primaddr_id:
                    if move_primary:
                        self._move_primary_email_address(address,
                                                         reassigned_addresses,
                                                         dest_et,
                                                         epat)
                else:
                    self._move_email_address(address, reassigned_addresses,
                                             dest_et)
        # Managing ad_email
        ad_emails_added = ""
        if domain_str == cereconf.NO_MAILBOX_DOMAIN_EMPLOYEES:
            ad = ad_email.AdEmail(self.db)

            if move_primary:
                ad_emails = ad.search_ad_email(account_name=source_uname)
                if len(ad_emails) == 1:
                    ad_emails_added = self._move_ad_email(ad_emails[0],
                                                          dest_uname)
            # TODO:
            #  If this command is called with move_primary=False,
            #  the source account's primary email address will be left
            #  intact, but it's corresponding ad_email will be deleted.
            #  This mimics the functionality of the uit-script move_emails.py,
            #  but is it really what we want?
            ad.delete_ad_email(account_name=source_uname)

        return ("OK, reassigned {}. ".format(reassigned_addresses)
                + ad_emails_added)
示例#9
0
class BofhdExtension(BofhdCommonMethods):
    all_commands = {}

    authz = BofhdAuth

    all_commands['misc_sms_password'] = Command(
        ('misc', 'sms_password'),
        AccountName(),
        SimpleString(optional=True, default='no'),
        fs=FormatSuggestion('Password sent to %s.', ('number', )),
        perm_filter='is_superuser')

    def misc_sms_password(self, operator, account_name, language='no'):
        u""" Send last password set for account in cache. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only superusers may send passwords by SMS")

        # Select last password change state entry for the victim
        try:
            state = filter(
                lambda k: account_name == k.get('account_id'),
                filter(lambda e: e.get('operation') == 'user_passwd',
                       self._get_cached_passwords(operator)))[-1]
        except IndexError:
            raise CerebrumError(
                'No password for {} in session'.format(account_name))

        # Get person object
        person = self._get_person('account_name', account_name)

        # Select phone number, filter out numbers from systems where we do not
        # have an affiliation.
        try:
            spec = map(
                lambda (s, t):
                (self.const.human2constant(s), self.const.human2constant(t)),
                cereconf.SMS_NUMBER_SELECTOR)
            mobile = person.sort_contact_info(spec, person.get_contact_info())
            person_in_systems = [
                int(af['source_system'])
                for af in person.list_affiliations(person_id=person.entity_id)
            ]
            mobile = filter(lambda x: x['source_system'] in person_in_systems,
                            mobile)[0]['contact_value']

        except IndexError:
            raise CerebrumError(
                'No applicable phone number for {}'.format(account_name))

        # Load and fill template for chosen language
        try:
            from os import path
            with open(
                    path.join(cereconf.TEMPLATE_DIR,
                              'password_sms_{}.template'.format(language)),
                    'r') as f:
                msg = f.read().format(account_name=account_name,
                                      password=state.get('password'))
        except IOError:
            raise CerebrumError(
                'Could not load template for language {}'.format(language))

        # Maybe send SMS
        if getattr(cereconf, 'SMS_DISABLE', False):
            self.logger.info('SMS disabled in cereconf, would have '
                             'sent password SMS to {}'.format(mobile))
        else:
            sms = SMSSender(logger=self.logger)
            if not sms(mobile, msg, confirm=True):
                raise CerebrumError(
                    'Unable to send message to {}, aborting'.format(mobile))

        return {'number': mobile}

    all_commands['misc_sms_message'] = Command(
        ('misc', 'sms_message'),
        AccountName(),
        SMSString(),
        fs=FormatSuggestion('Message sent to %s.', ('number', )),
        perm_filter='is_superuser')

    def misc_sms_message(self, operator, account_name, message):
        """
        Sends SMS message(s)
        """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may send messages by SMS')
        # Get person object
        person = self._get_person('account_name', account_name)
        # Select phone number, filter out numbers from systems where we do not
        # have an affiliation.
        try:
            spec = map(
                lambda (s, t):
                (self.const.human2constant(s), self.const.human2constant(t)),
                cereconf.SMS_NUMBER_SELECTOR)
            mobile = person.sort_contact_info(spec, person.get_contact_info())
            person_in_systems = [
                int(af['source_system'])
                for af in person.list_affiliations(person_id=person.entity_id)
            ]
            mobile = filter(lambda x: x['source_system'] in person_in_systems,
                            mobile)[0]['contact_value']
        except IndexError:
            raise CerebrumError(
                'No applicable phone number for {}'.format(account_name))
        # Send SMS
        if getattr(cereconf, 'SMS_DISABLE', False):
            self.logger.info('SMS disabled in cereconf, would have '
                             'sent password SMS to {}'.format(mobile))
        else:
            sms = SMSSender(logger=self.logger)
            if not sms(mobile, message, confirm=True):
                raise CerebrumError(
                    'Unable to send message to {}. Aborting.'.format(mobile))
        return {'number': mobile}
示例#10
0
class BofhdExtension(BofhdCommonMethods):
    u""" Extends bofhd with a 'note' command group. """

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'note': 'Entity note related commands',
        }

        command_help = {
            'note': {
                'note_show': 'Show notes associated with an entity',
                'note_add': 'Add a new note to an entity',
                'note_remove': 'Remove a note associated with an entity',
            },
        }

        arg_help = {
            'note_id':
                ['note_id', 'Enter note ID',
                 'Enter the ID of the note'],
            'note_subject':
                ['note_subject', 'Enter subject',
                 'Enter the subject of the note'],
            'note_description':
                ['note_description', 'Enter description',
                 'Enter the description of the note'],
        }

        return (group_help, command_help, arg_help)

    #
    # note show <subject-id>
    #
    all_commands['note_show'] = Command(
        ('note', 'show'),
        Id(help_ref='id:target:entity'),
        fs=FormatSuggestion([
            ("%d note(s) found for %s:\n", ("notes_total", "entity_target")),
            ("Note #%d added by %s on %s:\n"
             "%s: %s\n",
             ("note_id", "creator", format_time("create_date"),
              "subject", "description"))
        ]),
        perm_filter='can_show_notes')

    def note_show(self, operator, entity_target):
        self.ba.can_show_notes(operator.get_entity_id())

        entity = self.util.get_target(
            entity_target, default_lookup="account", restrict_to=[]
        )
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)

        notes = enote.get_notes()
        result = []

        if len(notes) is 0:
            return "No notes were found for %s" % (entity_target)
        else:
            result.append({
                'notes_total': len(notes),
                'entity_target': entity_target,
            })

        for note_row in notes:
            note = {}

            for key, value in note_row.items():
                note[key] = value

                if key in ('subject', 'description') and len(value) is 0:
                    note[key] = '<not set>'

            # translate creator_id to username
            acc = self._get_account(note_row['creator_id'], idtype='id')
            note['creator'] = acc.account_name
            result.append(note)

        return result

    #
    # note add <subject-id> <title> <contents>
    #
    all_commands['note_add'] = Command(
        ('note', 'add'),
        Id(help_ref='id:target:entity'),
        SimpleString(help_ref='note_subject'),
        SimpleString(help_ref='note_description'),
        perm_filter='can_add_notes')

    def note_add(self, operator, entity_target, subject, description):
        self.ba.can_add_notes(operator.get_entity_id())

        if len(subject) > 70:
            raise CerebrumError(
                u"Subject field cannot be longer than 70 characters")
        if len(description) > 1024:
            raise CerebrumError(
                u"Description field cannot be longer than 1024 characters")

        entity = self.util.get_target(entity_target, restrict_to=[])
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)
        note_id = enote.add_note(operator.get_entity_id(),
                                 subject,
                                 description)
        return "Note #%s was added to entity %s" % (note_id, entity_target)

    #
    # note remove <subject-id> <note-id>
    #
    all_commands['note_remove'] = Command(
        ('note', 'remove'),
        Id(help_ref='id:target:entity'),
        SimpleString(help_ref='note_id'),
        perm_filter='can_remove_notes')

    def note_remove(self, operator, entity_target, note_id):
        self.ba.can_remove_notes(operator.get_entity_id())

        entity = self.util.get_target(entity_target, restrict_to=[])
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)

        if not note_id.isdigit():
            raise CerebrumError("Note ID must be a numeric value")

        for note_row in enote.get_notes():
            if int(note_row['note_id']) is int(note_id):
                enote.delete_note(note_id)
                return "Note #%s associated with entity %s was removed" % (
                    note_id, entity_target)

        raise CerebrumError("Note #%s is not associated with entity %s" % (
            note_id, entity_target))
示例#11
0
class BofhdExtension(BofhdCommandBase):
    """Commands used for managing and inspecting events."""

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        """Definition of the help text for event-related commands."""
        group_help = {
            'event': "Event related commands",
        }

        # The texts in command_help are automatically line-wrapped, and should
        # not contain \n
        command_help = {
            'event': {
                'event_stat':
                'Show statistics about the events',
                'event_info':
                'Display an event',
                'event_list':
                'List locked and failed events',
                'event_force':
                'Force processing of a terminally failed event',
                'event_force_all':
                ('Force processing of all failed events for a '
                 'target system'),
                'event_unlock':
                'Unlock a previously locked event',
                'event_delete':
                'Delete an event',
                'event_search':
                'Search for events'
            },
        }

        arg_help = {
            'target_system': [
                'target_system', 'Target system (e.g. \'Exchange\')',
                'Enter the target system for this operation'
            ],
            'event_id':
            ['event_id', 'Event Id', 'The numerical identifier of an event'],
            'search_pattern': [
                'search_pattern', 'Search pattern',
                'Patterns that can be used:\n'
                '  id:0                   Matches events where dest- or '
                'subject_entity is set to 0\n'
                '  type:spread:add        Matches events of type spread:add\n'
                '  param:Joe              Matches events where the string'
                ' "Joe" is found in the change params\n'
                '  target_system:Exchange Matches events for a target system\n'
                '  from_ts:2016-01-01     Matches events from after a '
                'timestamp\n'
                '  to_ts:2016-12-31       Matches events from before a '
                'timestamp\n'
                'In combination, these patterns form a boolean AND expression.'
                '\nTimestamps can also be \'today\', \'yesterday\' or precise'
                ' to the second.'
            ]
        }

        return (group_help, command_help, arg_help)

    # Validate that the target system exists, and that the operator is
    # allowed to perform operations on it.
    def _validate_target_system(self, operator, target_sys):
        # TODO: Check for perms on target system.
        ts = self.const.TargetSystem(target_sys)
        try:
            # Provoke. See if it exists.
            int(ts)
        except Errors.NotFoundError:
            raise CerebrumError('No such target system: {}'.format(target_sys))
        return ts

    # Convert dictionary entries known to contain contant codes,
    # to human-readable text
    def _make_constants_human_readable(self, data):
        constant_keys = [
            'spread', 'entity_type', 'code', 'affiliation', 'src',
            'name_variant', 'action', 'level'
        ]
        try:
            for key in constant_keys:
                if key in data:
                    value = self.const.human2constant(data[key])
                    if value:
                        data[key] = str(value)
        except TypeError:
            pass
        return data

    # event stat
    all_commands['event_stat'] = Command((
        'event',
        'stat',
    ),
                                         TargetSystem(),
                                         fs=FormatSuggestion([
                                             (
                                                 'Total failed: %d\n'
                                                 'Total locked: %d\n'
                                                 'Total       : %d',
                                                 (
                                                     't_failed',
                                                     't_locked',
                                                     'total',
                                                 ),
                                             ),
                                         ]),
                                         perm_filter='is_postmaster')

    def event_stat(self, operator, target_sys):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, target_sys)

        fail_limit = eventconf.CONFIG[str(ts)]['fail_limit']
        return self.db.get_target_stats(ts, fail_limit)

    # event list
    all_commands['event_list'] = Command((
        'event',
        'list',
    ),
                                         TargetSystem(),
                                         SimpleString(optional=True),
                                         fs=FormatSuggestion(
                                             '%-8d %-28s %-25s %d',
                                             (
                                                 'id',
                                                 'type',
                                                 'taken',
                                                 'failed',
                                             ),
                                             hdr='%-8s %-28s %-25s %s' % (
                                                 'Id',
                                                 'Type',
                                                 'Taken',
                                                 'Failed',
                                             ),
                                         ),
                                         perm_filter='is_postmaster')

    def event_list(self, operator, target_sys, args='failed'):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, target_sys)

        r = []
        # TODO: Check auth on target-system
        #       Remove perm_filter when this is implemented?
        if args == 'failed':
            fail_limit = eventconf.CONFIG[str(ts)]['fail_limit']
            locked = True
        elif args == 'full':
            fail_limit = None
            locked = False
        else:
            return []

        for ev in self.db.get_failed_and_locked_events(target_system=ts,
                                                       fail_limit=fail_limit,
                                                       locked=locked):
            r += [{
                'id': ev['event_id'],
                'type': str(self.const.map_const(ev['event_type'])),
                'taken': str(ev['taken_time']).replace(' ', '_'),
                'failed': ev['failed']
            }]
        return r

    # event force_all
    all_commands['event_force_all'] = Command(
        (
            'event',
            'force_all',
        ),
        TargetSystem(),
        fs=FormatSuggestion('Forced %d events', ('rowcount', )),
        perm_filter='is_postmaster')

    def event_force_all(self, operator, ts):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, ts)
        rowcount = self.db.reset_failed_counts_for_target_system(ts)
        return {'rowcount': rowcount}

    # event force
    all_commands['event_force'] = Command(
        (
            'event',
            'force',
        ),
        EventId(),
        fs=FormatSuggestion('Forcing %s', ('state', )),
        perm_filter='is_postmaster')

    def event_force(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        try:
            self.db.reset_failed_count(event_id)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    # event unlock
    all_commands['event_unlock'] = Command(
        (
            'event',
            'unlock',
        ),
        EventId(),
        fs=FormatSuggestion('Unlock %s', ('state', )),
        perm_filter='is_postmaster')

    def event_unlock(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        try:
            self.db.release_event(event_id, increment=False)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    # event delete
    all_commands['event_delete'] = Command(
        (
            'event',
            'delete',
        ),
        EventId(),
        fs=FormatSuggestion('Deletion %s', ('state', )),
        perm_filter='is_postmaster')

    def event_delete(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        try:
            self.db.remove_event(event_id)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    # event info
    all_commands['event_info'] = Command(
        (
            'event',
            'info',
        ),
        EventId(),
        fs=FormatSuggestion(
            'Event ID:           %d\n'
            'Event type:         %s\n'
            'Target system:      %s\n'
            'Failed attempts:    %d\n'
            'Time added:         %s\n'
            'Time taken:         %s\n'
            'Subject entity:     %s\n'
            'Destination entity: %s\n'
            'Parameters:         %s',
            ('event_id', 'event_type', 'target_system', 'failed', 'tstamp',
             'taken_time', 'subject_entity', 'dest_entity', 'change_params')),
        perm_filter='is_postmaster')

    def event_info(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        try:
            ev = self.db.get_event(event_id)
        except Errors.NotFoundError:
            raise CerebrumError('Error: No such event exists!')

        # For certain keys, convert constants to human-readable representations
        change_params = ev['change_params']
        if change_params:
            change_params = pickle.loads(ev['change_params'])
            change_params = self._make_constants_human_readable(change_params)

        ret = {
            'event_id':
            ev['event_id'],
            'event_type':
            str(self.const.map_const(ev['event_type'])),
            'target_system':
            str(self.const.map_const(ev['target_system'])),
            'failed':
            ev['failed'],
            'tstamp':
            str(ev['tstamp']),
            'taken_time':
            str(ev['taken_time']) if ev['taken_time'] else '<not set>',
            'subject_entity':
            str(ev['subject_entity']) if ev['subject_entity'] else '<not set>',
            'dest_entity':
            str(ev['dest_entity']) if ev['dest_entity'] else '<not set>',
            'change_params':
            repr(change_params) if change_params else '<not set>'
        }

        en = Factory.get('Entity')(self.db)

        # Look up types and names for subject and destination entities
        for key in ('subject_entity', 'dest_entity'):
            if ev[key]:
                try:
                    en.clear()
                    en.find(ev[key])
                    entity_type = str(self.const.map_const(en.entity_type))
                    entity_name = self._get_entity_name(
                        en.entity_id, en.entity_type)
                    ret[key] = '{} {} (id:{:d})'.format(
                        entity_type, entity_name, en.entity_id)
                except:
                    pass

        return ret

    # event search
    all_commands['event_search'] = Command(
        (
            'event',
            'search',
        ),
        SimpleString(repeat=True, help_ref='search_pattern'),
        fs=FormatSuggestion(
            '%-8d %-35s %-15s %-15s %-25s %-6d %s',
            ('id', 'type', 'subject_type', 'dest_type', 'taken', 'failed',
             'params'),
            hdr='%-8s %-35s %-15s %-15s %-25s %-6s %s' %
            ('Id', 'Type', 'SubjectType', 'DestinationType', 'Taken', 'Failed',
             'Params'),
        ),
        perm_filter='is_postmaster')

    def event_search(self, operator, *args):
        """Search for events in the database.

        :param str search_str: A pattern to search for.
        """
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        # TODO: Fetch an ACL of which target systems can be searched by this
        # TODO: Make the param-search handle spaces and stuff?
        params = {}

        # Parse search patterns
        for arg in args:
            try:
                key, value = arg.split(':', 1)
            except ValueError:
                # Ignore argument
                continue
            if key == 'type':
                try:
                    cat, typ = value.split(':')
                except ValueError:
                    raise CerebrumError('Search pattern incomplete')
                try:
                    type_code = int(self.const.ChangeType(cat, typ))
                except Errors.NotFoundError:
                    raise CerebrumError('EventType does not exist')
                params['type'] = type_code
            elif key == 'target_system':
                ts = self._validate_target_system(operator, value)
                params['target_system'] = ts
            else:
                params[key] = value

        # Raise if we do not have any search patterns
        if not params:
            raise CerebrumError('Must specify search pattern.')

        # Fetch matching event ids
        try:
            event_ids = self.db.search_events(**params)
        except TypeError:
            # If the user spells an argument wrong, we tell them that they have
            # done so. They can always type '?' at the pattern-prompt to get
            # help.
            raise CerebrumError('Invalid arguments')
        except self.db.DataError as e:
            # E.g. bogus timestamp
            message = e.args[0].split("\n")[0]
            raise CerebrumError('Database does not approve: ' + message)

        # Fetch information about the event ids, and present it to the user.
        r = []
        for event_id in event_ids:
            ev = self.db.get_event(event_id['event_id'])
            try:
                types = self.db.get_event_target_type(event_id['event_id'])
            except Errors.NotFoundError:
                # If we wind up here, both the subject- or destination-entity
                # has been deleted, or the event does not carry information
                # about a subject- or destination-entity.
                types = []

            change_params = ev['change_params']
            if ev['change_params']:
                change_params = pickle.loads(ev['change_params'])
                change_params = self._make_constants_human_readable(
                    change_params)

            ret = {
                'id': ev['event_id'],
                'type': str(self.const.map_const(ev['event_type'])),
                'taken': str(ev['taken_time']).replace(' ', '_'),
                'failed': ev['failed'],
                'params': repr(change_params),
                'dest_type': None,
                'subject_type': None,
            }

            if 'dest_type' in types:
                ret['dest_type'] = str(self.const.map_const(
                    types['dest_type']))
            if 'subject_type' in types:
                ret['subject_type'] = str(
                    self.const.map_const(types['subject_type']))
            r.append(ret)
        return r
示例#12
0
class BofhdExtension(BofhdCommandBase):
    """Commands used for managing and inspecting events."""

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        """Definition of the help text for event-related commands."""
        _, _, args = get_help_strings()
        return merge_help_strings(
            ({}, {}, args),
            (HELP_EVENT_GROUP, HELP_EVENT_CMDS, HELP_EVENT_ARGS))

    def _validate_target_system(self, operator, target_sys):
        """Validate that the target system exists."""
        # TODO: Check for perms on target system.
        ts = self.const.TargetSystem(target_sys)
        try:
            # Provoke. See if it exists.
            int(ts)
        except Errors.NotFoundError:
            raise CerebrumError('No such target system: %r' % target_sys)
        return ts

    def _make_constants_human_readable(self, data):
        """Convert dictionary entries known to contain contant codes,
        to human-readable text."""
        constant_keys = [
            'spread', 'entity_type', 'code', 'affiliation', 'src',
            'name_variant', 'action', 'level'
        ]
        try:
            for key in constant_keys:
                if key in data:
                    value = self.const.human2constant(data[key])
                    if value:
                        data[key] = six.text_type(value)
        except TypeError:
            pass
        return data

    def _parse_search_params(self, *args):
        """Convert string search pattern to a dict."""
        # TODO: Make the param-search handle spaces and stuff?
        params = defaultdict(list)
        for arg in args:
            try:
                key, value = arg.split(':', 1)
            except ValueError:
                # Ignore argument
                continue
            if key == 'type':
                try:
                    cat, typ = value.split(':')
                except ValueError:
                    raise CerebrumError('Search pattern incomplete')
                try:
                    type_code = int(self.clconst.ChangeType(cat, typ))
                except Errors.NotFoundError:
                    raise CerebrumError('EventType does not exist')
                params['type'].append(type_code)
            elif key == 'is_taken':
                params[key] = self._get_boolean(value)
            else:
                params[key] = value
        return params

    def _search_events(self, **params):
        """Find rows matching search params."""
        try:
            return self.db.search_events(**params)
        except TypeError:
            # If the user spells an argument wrong, we tell them that they have
            # done so. They can always type '?' at the pattern-prompt to get
            # help.
            raise CerebrumError('Invalid arguments')
        except self.db.DataError as e:
            # E.g. bogus timestamp
            message = e.args[0].split("\n")[0]
            raise CerebrumError('Database does not approve: ' + message)

    #
    # event stat
    #
    all_commands['event_stat'] = Command((
        'event',
        'stat',
    ),
                                         TargetSystem(),
                                         fs=FormatSuggestion([
                                             (
                                                 'Total failed: %d\n'
                                                 'Total locked: %d\n'
                                                 'Total       : %d',
                                                 (
                                                     't_failed',
                                                     't_locked',
                                                     'total',
                                                 ),
                                             ),
                                         ]),
                                         perm_filter='is_postmaster')

    def event_stat(self, operator, target_sys):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, target_sys)

        fail_limit = eventconf.CONFIG[six.text_type(ts)]['fail_limit']
        return self.db.get_target_stats(ts, fail_limit)

    #
    # event list
    #
    all_commands['event_list'] = Command(
        (
            'event',
            'list',
        ),
        TargetSystem(),
        SimpleString(help_ref='event_list_filter', optional=True),
        fs=FormatSuggestion(
            '%-8d %-35s %-22s %d',
            (
                'id',
                'type',
                format_time('taken'),
                'failed',
            ),
            hdr='%-8s %-35s %-22s %s' % (
                'Id',
                'Type',
                'Taken',
                'Failed',
            ),
        ),
        perm_filter='is_postmaster')

    def event_list(self, operator, target_sys, args='failed'):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, target_sys)

        r = []
        # TODO: Check auth on target-system
        #       Remove perm_filter when this is implemented?
        if args == 'failed':
            fail_limit = eventconf.CONFIG[six.text_type(ts)]['fail_limit']
            locked = True
        elif args == 'full':
            fail_limit = None
            locked = False
        else:
            return []

        for ev in self.db.get_failed_and_locked_events(target_system=ts,
                                                       fail_limit=fail_limit,
                                                       locked=locked):
            r.append({
                'id':
                ev['event_id'],
                'type':
                six.text_type(self.clconst.map_const(ev['event_type'])),
                'taken':
                ev['taken_time'],
                'failed':
                ev['failed']
            })
        return r

    #
    # event force_all
    #
    all_commands['event_force_all'] = Command(
        (
            'event',
            'force_all',
        ),
        TargetSystem(),
        fs=FormatSuggestion('Forced %d events', ('rowcount', )),
        perm_filter='is_postmaster')

    def event_force_all(self, operator, ts):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        ts = self._validate_target_system(operator, ts)
        rowcount = self.db.reset_failed_counts_for_target_system(ts)
        return {'rowcount': rowcount}

    #
    # event force
    #
    all_commands['event_force'] = Command(
        (
            'event',
            'force',
        ),
        EventId(),
        fs=FormatSuggestion('Forcing %s', ('state', )),
        perm_filter='is_postmaster')

    def event_force(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        try:
            self.db.reset_failed_count(event_id)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    #
    # event unlock
    #
    all_commands['event_unlock'] = Command(
        (
            'event',
            'unlock',
        ),
        EventId(),
        fs=FormatSuggestion('Unlock %s', ('state', )),
        perm_filter='is_postmaster')

    def event_unlock(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')
        try:
            self.db.release_event(event_id, increment=False)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    # event delete
    all_commands['event_delete'] = Command(
        (
            'event',
            'delete',
        ),
        EventId(),
        fs=FormatSuggestion('Deletion %s', ('state', )),
        perm_filter='is_postmaster')

    def event_delete(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        try:
            self.db.remove_event(event_id)
            state = True
        except Errors.NotFoundError:
            state = False
        return {'state': 'failed' if not state else 'succeeded'}

    # event delete_where
    all_commands['event_delete_where'] = Command(
        (
            'event',
            'delete_where',
        ),
        TargetSystem(),
        SimpleString(repeat=True, help_ref='search_pattern'),
        fs=FormatSuggestion(
            'Deleted %s of %s matching events (%s failed/vanished)',
            ('success', 'total', 'failed')),
        perm_filter='is_postmaster')

    def event_delete_where(self, operator, target_sys, *args):
        """Delete events matching a search query.

        :param str target_sys: Target system to search
        :param str args: Pattern(s) to search for.
        """
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        # TODO: Fetch an ACL of which target systems can be searched by this
        ts = self._validate_target_system(operator, target_sys)

        params = self._parse_search_params(*args)
        if not params:
            raise CerebrumError('Must specify search pattern.')

        params['target_system'] = ts
        event_ids = [row['event_id'] for row in self._search_events(**params)]
        stats = {}
        stats['total'] = len(event_ids)
        stats['success'] = stats['failed'] = 0

        for event_id in event_ids:
            try:
                self.db.remove_event(event_id)
                stats['success'] += 1
            except Errors.NotFoundError:
                stats['failed'] += 1
        return stats

    # event unlock_where
    all_commands['event_unlock_where'] = Command(
        (
            'event',
            'unlock_where',
        ),
        TargetSystem(),
        SimpleString(repeat=True, help_ref='search_pattern'),
        fs=FormatSuggestion(
            'Unlocked %d of %d matching events (%d failed/vanished)',
            ('success', 'total', 'failed')),
        perm_filter='is_postmaster')

    def event_unlock_where(self, operator, target_sys, *args):
        """Unlock events matching a search query.

        :param str target_sys: Target system to search
        :param str args: Pattern(s) to search for.
        """
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        # TODO: Fetch an ACL of which target systems can be searched by this
        ts = self._validate_target_system(operator, target_sys)

        params = self._parse_search_params(*args)
        if not params:
            raise CerebrumError('Must specify search pattern.')

        params['is_taken'] = True
        params['target_system'] = ts
        event_ids = [row['event_id'] for row in self._search_events(**params)]
        stats = {}
        stats['total'] = len(event_ids)
        stats['success'] = stats['failed'] = 0

        for event_id in event_ids:
            try:
                self.db.release_event(event_id, increment=False)
                stats['success'] += 1
            except Errors.NotFoundError:
                stats['failed'] += 1
        return stats

    # event info
    all_commands['event_info'] = Command(
        (
            'event',
            'info',
        ),
        EventId(),
        fs=FormatSuggestion(
            'Event ID:           %d\n'
            'Event type:         %s\n'
            'Target system:      %s\n'
            'Failed attempts:    %d\n'
            'Time added:         %s\n'
            'Time taken:         %s\n'
            'Subject entity:     %s\n'
            'Destination entity: %s\n'
            'Parameters:         %s',
            ('event_id', 'event_type', 'target_system', 'failed',
             format_time('tstamp'), format_time('taken_time'),
             'subject_entity', 'dest_entity', 'change_params')),
        perm_filter='is_postmaster')

    def event_info(self, operator, event_id):
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        try:
            ev = self.db.get_event(event_id)
        except ValueError:
            raise CerebrumError("Error: Event id must be an integer")
        except Errors.NotFoundError:
            raise CerebrumError('Error: No such event exists!')

        # For certain keys, convert constants to human-readable representations
        change_params = ev['change_params']
        if change_params:
            change_params = json.loads(ev['change_params'])
            change_params = self._make_constants_human_readable(change_params)
            change_params = repr(change_params)
        else:
            change_params = None

        ret = {
            'event_id':
            ev['event_id'],
            'event_type':
            six.text_type(self.clconst.map_const(ev['event_type'])),
            'target_system':
            six.text_type(self.const.map_const(ev['target_system'])),
            'failed':
            ev['failed'],
            'tstamp':
            ev['tstamp'],
            'taken_time':
            ev['taken_time'],
            'subject_entity':
            ev['subject_entity'],
            'dest_entity':
            ev['dest_entity'],
            'change_params':
            change_params,
        }

        en = Factory.get('Entity')(self.db)

        # Look up types and names for subject and destination entities
        for key in ('subject_entity', 'dest_entity'):
            if ev[key]:
                try:
                    en.clear()
                    en.find(ev[key])
                    entity_type = six.text_type(
                        self.const.map_const(en.entity_type))
                    entity_name = self._get_entity_name(
                        en.entity_id, en.entity_type)
                    ret[key] = '{} {} (id:{:d})'.format(
                        entity_type, entity_name, en.entity_id)
                except Exception:
                    pass

        return ret

    #
    # event search
    #
    all_commands['event_search'] = Command(
        (
            'event',
            'search',
        ),
        TargetSystem(),
        SimpleString(repeat=True, help_ref='search_pattern'),
        fs=FormatSuggestion(
            '%-8d %-35s %-15s %-15s %-22s %-6d %s',
            ('id', 'type', 'subject_type', 'dest_type', format_time('taken'),
             'failed', 'params'),
            hdr='%-8s %-35s %-15s %-15s %-22s %-6s %s' %
            ('Id', 'Type', 'SubjectType', 'DestinationType', 'Taken', 'Failed',
             'Params'),
        ),
        perm_filter='is_postmaster')

    def event_search(self, operator, target_sys, *args):
        """Search for events in the database.

        :param str target_sys: Target system to search
        :param str args: Pattern(s) to search for.
        """
        if not self.ba.is_postmaster(operator.get_entity_id()):
            raise PermissionDenied('No access to event')

        # TODO: Fetch an ACL of which target systems can be searched by this
        ts = self._validate_target_system(operator, target_sys)

        params = self._parse_search_params(*args)
        if not params:
            raise CerebrumError('Must specify search pattern.')

        params['target_system'] = ts
        event_ids = self._search_events(**params)

        # Fetch information about the event ids, and present it to the user.
        r = []
        for event_id in event_ids:
            ev = self.db.get_event(event_id['event_id'])
            try:
                types = self.db.get_event_target_type(event_id['event_id'])
            except Errors.NotFoundError:
                # If we wind up here, both the subject- or destination-entity
                # has been deleted, or the event does not carry information
                # about a subject- or destination-entity.
                types = []

            change_params = ev['change_params']
            if ev['change_params']:
                change_params = json.loads(ev['change_params'])
                change_params = self._make_constants_human_readable(
                    change_params)

            ret = {
                'id': ev['event_id'],
                'type':
                six.text_type(self.clconst.map_const(ev['event_type'])),
                'taken': ev['taken_time'],
                'failed': ev['failed'],
                'params': repr(change_params),
                'dest_type': None,
                'subject_type': None,
            }

            if 'dest_type' in types:
                ret['dest_type'] = six.text_type(
                    self.const.map_const(types['dest_type']))
            if 'subject_type' in types:
                ret['subject_type'] = six.text_type(
                    self.const.map_const(types['subject_type']))
            r.append(ret)
        return r
示例#13
0
class BofhdExtension(BofhdCommonMethods):

    all_commands = {}
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        # No GROUP_HELP, we'll just guess that 'user' and 'misc' exists
        # already.
        # The help structure doesn't really allow command groups to come from
        # different bofhd-extensions, so if we were to implement texts for
        # these groups, they might override texts defined elsewhere...
        return ({}, CMD_HELP, CMD_ARGS)

    def _get_phone_number(self, person_id, phone_types):
        """Search through a person's contact info and return the first found info
        value as defined by the given types and source systems.

        @type  person_id: integer
        @param person_id: Entity ID of the person

        @type  phone_types: list
        @param phone_types: list containing pairs of the source system and
                            contact type to look for, i.e. on the form:
                            (_AuthoritativeSystemCode, _ContactInfoCode)
        """
        person = self._get_person('entity_id', person_id)
        for sys, type in phone_types:
            for row in person.get_contact_info(source=sys, type=type):
                return row['contact_value']

        return None

    def _select_sms_number(self, account_name):
        """Find the best matching mobile number for SMS.

        Search through an account owners contact info and return the best match
        according to cereconf.SMS_NUMBER_SELECTOR

        :param str account_name: account name
        :return str: mobile phone number
        """
        person = self._get_person('account_name', account_name)
        try:
            spec = map(lambda (s, t): (self.const.human2constant(s),
                                       self.const.human2constant(t)),
                       cereconf.SMS_NUMBER_SELECTOR)
            mobile = person.sort_contact_info(spec, person.get_contact_info())
            person_in_systems = [int(af['source_system']) for af in
                                 person.list_affiliations(
                                     person_id=person.entity_id)]
            return filter(lambda x: x['source_system'] in person_in_systems,
                          mobile)[0]['contact_value']

        except IndexError:
            raise CerebrumError('No applicable phone number for %r' %
                                account_name)

    #
    # user send_welcome_sms <accountname> [<mobile override>]
    #
    all_commands['user_send_welcome_sms'] = Command(
        ("user", "send_welcome_sms"),
        AccountName(help_ref="account_name", repeat=False),
        Mobile(help_ref='welcome_mobile', optional=True),
        fs=FormatSuggestion([('Ok, message sent to %s', ('mobile',)), ]),
        perm_filter='can_send_welcome_sms')

    def user_send_welcome_sms(self, operator, username, mobile=None):
        """Send a (new) welcome SMS to a user.

        Optional mobile override, if what's registered in Cerebrum is wrong or
        missing. Override must be permitted in the cereconf setting
        BOFHD_ALLOW_MANUAL_MOBILE.
        """
        sms = SMSSender(logger=self.logger)
        account = self._get_account(username)
        # Access Control
        self.ba.can_send_welcome_sms(operator.get_entity_id())
        # Ensure allowed to specify a phone number
        if not cereconf.BOFHD_ALLOW_MANUAL_MOBILE and mobile:
            raise CerebrumError('Not allowed to specify number')
        # Ensure proper formatted phone number
        if mobile and not (len(mobile) == 8 and mobile.isdigit()):
            raise CerebrumError('Invalid phone number, must be 8 digits')
        # Ensure proper account
        if account.is_deleted():
            raise CerebrumError("User is deleted")
        if account.is_expired():
            raise CerebrumError("User is expired")
        if account.owner_type != self.const.entity_person:
            raise CerebrumError("User is not a personal account")
        # Look up the mobile number
        if not mobile:
            phone_types = [(self.const.system_sap,
                            self.const.contact_private_mobile),
                           (self.const.system_sap,
                            self.const.contact_mobile_phone),
                           (self.const.system_fs,
                            self.const.contact_mobile_phone)]
            mobile = self._get_phone_number(account.owner_id, phone_types)
            if not mobile:
                raise CerebrumError("No mobile phone number for %r" % username)
        # Get primary e-mail address, if it exists
        mailaddr = ''
        try:
            mailaddr = account.get_primary_mailaddress()
        except:
            pass
        # NOTE: There's no need to supply the 'email' entry at the moment,
        # but contrib/no/send_welcome_sms.py does it as well
        # TODO: The whole templating system is getting re-worked, ~tgk can deal
        # with this...
        message = cereconf.AUTOADMIN_WELCOME_SMS % {"username": username,
                                                    "studentnumber": '',
                                                    "email": mailaddr}
        if not sms(mobile, message):
            raise CerebrumError("Could not send SMS to %r" % mobile)

        # Set sent sms welcome sent-trait, so that it will be ignored by the
        # scheduled job for sending welcome-sms.
        try:
            account.delete_trait(self.const.trait_sms_welcome)
        except Errors.NotFoundError:
            pass
        finally:
            account.populate_trait(code=self.const.trait_sms_welcome,
                                   date=DateTime.now())
            account.write_db()
        return {'mobile': mobile}

    #
    # misc sms_password <username> [lang]
    #
    all_commands['misc_sms_password'] = Command(
        ('misc', 'sms_password'),
        AccountName(help_ref="account_name", repeat=False),
        SimpleString(help_ref='sms_pass_lang', repeat=False,
                     optional=True, default='no'),
        fs=FormatSuggestion('Password sent to %s.', ('number',)),
        perm_filter='is_superuser')

    def misc_sms_password(self, operator, account_name, language='no'):
        """Send last password set for account in cache."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only superusers may send passwords by SMS")

        # Select last password change state entry for the victim
        try:
            state = filter(
                lambda k: account_name == k.get('account_id'),
                filter(lambda e: e.get('operation') == 'user_passwd',
                       self._get_cached_passwords(operator)))[-1]
        except IndexError:
            raise CerebrumError('No password for %r in session' % account_name)

        mobile = self._select_sms_number(account_name)

        # Load and fill template for chosen language
        # TODO: The whole templating system is getting re-worked, ~tgk can deal
        # with this...
        try:
            with io.open(
                    os.path.join(
                        cereconf.TEMPLATE_DIR,
                        'password_sms_{}.template'.format(language)),
                    'r',
                    encoding='utf-8') as f:
                msg = f.read().format(
                    account_name=account_name,
                    password=state.get('password'))
        except IOError:
            raise CerebrumError(
                'Could not load template for language {}'.format(language))

        # Maybe send SMS
        if getattr(cereconf, 'SMS_DISABLE', False):
            self.logger.info(
                'SMS disabled in cereconf, would have '
                'sent password to %r', mobile)
        else:
            sms = SMSSender(logger=self.logger)
            if not sms(mobile, msg):
                raise CerebrumError('Unable to send message to %r' % mobile)

        return {'number': mobile}

    #
    # misc sms_message <username> <message>
    #
    all_commands['misc_sms_message'] = Command(
        ('misc', 'sms_message'),
        AccountName(help_ref='account_name'),
        SMSString(help_ref='sms_message', repeat=False),
        fs=FormatSuggestion('Message sent to %s.', ('number',)),
        perm_filter='is_superuser')

    def misc_sms_message(self, operator, account_name, message):
        """
        Sends SMS message(s)
        """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may send messages by SMS')

        mobile = self._select_sms_number(account_name)

        # Send SMS
        if getattr(cereconf, 'SMS_DISABLE', False):
            self.logger.info(
                'SMS disabled in cereconf, would have '
                'sent message to %r', mobile)
        else:
            sms = SMSSender(logger=self.logger)
            if not sms(mobile, message):
                raise CerebrumError('Unable to send message to %r' % mobile)
        return {'number': mobile}
class BofhdApiKeyCommands(BofhdCommandBase):
    """API subscription commands."""

    all_commands = {}
    authz = BofhdApiKeyAuth

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        return merge_help_strings(
            (HELP_GROUP, HELP_CMD, HELP_ARGS),
            get_help_strings())

    #
    # api subscription_set <identifier> <account> [description]
    #
    all_commands['api_subscription_set'] = Command(
        ('api', 'subscription_set'),
        SimpleString(help_ref='api_client_identifier'),
        AccountName(),
        SimpleString(help_ref='api_client_description', optional=True),
        fs=FormatSuggestion(
            "Set subscription='%s' for account %s (%d)",
            ('identifier', 'account_name', 'account_id')
        ),
        perm_filter='can_modify_api_mapping',
    )

    def api_subscription_set(self, operator, identifier, account_id,
                             description=None):
        """Set api client identifier to user mapping"""
        # check araguments
        if not identifier:
            raise CerebrumError("Invalid identifier")
        account = self._get_account(account_id)

        # check permissions
        self.ba.can_modify_api_mapping(operator.get_entity_id(),
                                       account=account)

        keys = ApiMapping(self.db)
        try:
            row = keys.get(identifier)
            if row['account_id'] != account.entity_id:
                raise CerebrumError("Identifier already assigned")
        except Errors.NotFoundError:
            pass

        keys.set(identifier, account.entity_id, description)
        return {
            'identifier': identifier,
            'account_id': account.entity_id,
            'account_name': account.account_name,
            'description': description,
        }

    #
    # api subscription_clear <identifier>
    #
    all_commands['api_subscription_clear'] = Command(
        ('api', 'subscription_clear'),
        SimpleString(help_ref='api_client_identifier'),
        fs=FormatSuggestion(
            "Cleared subscription='%s' from account %s (%d)",
            ('identifier', 'account_name', 'account_id')
        ),
        perm_filter='can_modify_api_mapping',
    )

    def api_subscription_clear(self, operator, identifier):
        """Remove mapping for a given api client identifier"""
        if not identifier:
            raise CerebrumError("Invalid identifier")

        keys = ApiMapping(self.db)
        try:
            mapping = keys.get(identifier)
        except Errors.NotFoundError:
            raise CerebrumError("Unknown subscription identifier %r" %
                                (identifier,))

        # check permissions
        account = self.Account_class(self.db)
        account.find(mapping['account_id'])
        self.ba.can_modify_api_mapping(operator.get_entity_id(),
                                       account=account)

        if not mapping:
            raise CerebrumError('No identifier=%r for account %s (%d)' %
                                (identifier, account.account_name,
                                 account.entity_id))
        keys.delete(identifier)
        return {
            'identifier': mapping['identifier'],
            'account_id': account.entity_id,
            'account_name': account.account_name,
            'description': mapping['description'],
        }

    #
    # api subscription_list <account>
    #
    all_commands['api_subscription_list'] = Command(
        ('api', 'subscription_list'),
        AccountName(),
        fs=FormatSuggestion(
            "%-36s %-16s %s",
            ('identifier', format_time('updated_at'), 'description'),
            hdr="%-36s %-16s %s" % ('Identifier', 'Last update', 'Description')
        ),
        perm_filter='can_list_api_mapping',
    )

    def api_subscription_list(self, operator, account_id):
        """List api client mappings for a given user."""
        account = self._get_account(account_id)
        self.ba.can_list_api_mapping(operator.get_entity_id(), account=account)
        keys = ApiMapping(self.db)

        return [
            {
                'account_id': row['account_id'],
                'identifier': row['identifier'],
                # TODO: Add support for naive and localized datetime objects in
                # native_to_xmlrpc
                'updated_at': datetime2mx(row['updated_at']),
                'description': row['description'],
            }
            for row in keys.search(account_id=account.entity_id)
        ]

    #
    # api subscription_info <identifier>
    #
    all_commands['api_subscription_info'] = Command(
        ('api', 'subscription_info'),
        SimpleString(help_ref='api_client_identifier'),
        fs=FormatSuggestion(
            "\n".join((
                "Identifier:  %s",
                "Account:     %s (%d)",
                "Last update: %s",
                "Description: %s",
            )),
            ('identifier', 'account_name', 'account_id',
             format_time('updated_at'), 'description'),
        ),
        perm_filter='can_list_api_mapping',
    )

    def api_subscription_info(self, operator, identifier):
        """List api client mappings for a given user."""
        if not self.ba.can_list_api_mapping(operator.get_entity_id(),
                                            query_run_any=True):
            # Abort early if user has no access to list *any* api mappings,
            # otherwise we may leak info on valid identifiers.
            raise no_access_error

        keys = ApiMapping(self.db)
        try:
            mapping = keys.get(identifier)
        except Errors.NotFoundError:
            raise CerebrumError("Unknown subscription identifier %r" %
                                (identifier,))
        account = self.Account_class(self.db)
        account.find(mapping['account_id'])
        self.ba.can_list_api_mapping(operator.get_entity_id(), account=account)

        return {
            'account_id': account.entity_id,
            'account_name': account.account_name,
            'identifier': mapping['identifier'],
            # TODO: Add support for naive and localized datetime objects in
            # native_to_xmlrpc
            'updated_at': datetime2mx(mapping['updated_at']),
            'description': mapping['description'],
        }
示例#15
0
class BofhdExtension(BofhdCommandBase):
    """Class with 'user create_unpersonal' method."""

    all_commands = {}
    authz = BofhdUnpersonalAuth

    @classmethod
    def get_help_strings(cls):
        const = Factory.get('Constants')()
        account_types = const.fetch_constants(const.Account)
        cmd_args = {}
        list_sep = '\n - '
        for key, value in CMD_ARGS.items():
            cmd_args[key] = value[:]
            if key == 'unpersonal_account_type':
                cmd_args[key][2] += '\nValid account types:'
                cmd_args[key][2] += list_sep + list_sep.join(
                    six.text_type(c) for c in account_types)
        del const
        return merge_help_strings(
            ({}, {}, cmd_args),  # We want _our_ cmd_args to win!
            get_help_strings(),
            ({}, CMD_HELP, {}))

    #
    # user create_unpersonal
    #
    all_commands['user_create_unpersonal'] = Command(
        ('user', 'create_unpersonal'),
        AccountName(),
        GroupName(),
        EmailAddress(),
        SimpleString(help_ref="unpersonal_account_type"),
        fs=FormatSuggestion("Created account_id=%i", ("account_id",)),
        perm_filter='can_create_user_unpersonal')

    def user_create_unpersonal(self, operator,
                               account_name, group_name,
                               contact_address, account_type):
        """Bofh command: user create_unpersonal"""
        self.ba.can_create_user_unpersonal(operator.get_entity_id(),
                                           group=self._get_group(group_name))

        account_type = self._get_constant(self.const.Account, account_type,
                                          "account type")

        account_policy = AccountPolicy(self.db)
        try:
            account = account_policy.create_group_account(
                operator.get_entity_id(),
                account_name,
                self._get_group(group_name),
                contact_address,
                account_type
            )
        except InvalidAccountCreationArgument as e:
            raise CerebrumError(e)

        self._user_password(operator, account)

        # TBD: Better way of checking if email forwards are in use, by
        # checking if bofhd command is available?
        if hasattr(self, '_email_create_forward_target'):
            localaddr = '{}@{}'.format(
                account_name,
                Email.get_primary_default_email_domain())
            self._email_create_forward_target(localaddr, contact_address)

        return {'account_id': int(account.entity_id)}
示例#16
0
class BofhdExtension(BofhdCommonMethods):
    """Commands for managing Feide services and multifactor authentication."""

    hidden_commands = {}  # Not accessible through bofh
    all_commands = {}
    parent_commands = False
    authz = BofhdFeideAuth

    def _find_service(self, service_name):
        fse = FeideService(self.db)
        try:
            fse.find_by_name(service_name)
        except Errors.NotFoundError:
            raise CerebrumError('No such Feide service')
        return fse

    @classmethod
    def get_help_strings(cls):
        """ Help strings for Feide commands. """
        return (HELP_FEIDE_GROUP, HELP_FEIDE_CMDS, HELP_FEIDE_ARGS)

    #
    # feide service_add
    #
    all_commands['feide_service_add'] = Command(
        ('feide', 'service_add'),
        Integer(help_ref='feide_service_id'),
        SimpleString(help_ref='feide_service_name'),
        perm_filter='is_superuser')

    def feide_service_add(self, operator, feide_id, service_name):
        """ Add a Feide service """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may add Feide services')
        if not feide_id.isdigit():
            raise CerebrumError('Feide ID can only contain digits.')
        fse = FeideService(self.db)
        service_name = service_name.strip()
        name_error = fse.illegal_name(service_name)
        if name_error:
            raise CerebrumError(name_error)
        for service in fse.search():
            if int(feide_id) == int(service['feide_id']):
                raise CerebrumError(
                    'A Feide service with that ID already exists')
            if service_name == service['name']:
                raise CerebrumError(
                    'A Feide service with that name already exists')
        fse.populate(feide_id, service_name)
        fse.write_db()
        return "Added Feide service '{}'".format(service_name)

    #
    # feide service_remove
    #
    all_commands['feide_service_remove'] = Command(
        ('feide', 'service_remove'),
        SimpleString(help_ref='feide_service_name'),
        YesNo(help_ref='feide_service_confirm_remove'),
        perm_filter='is_superuser')

    def feide_service_remove(self, operator, service_name, confirm):
        """ Remove a Feide service. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may remove Feide services')
        if not confirm:
            return 'No action taken.'
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        fse.delete()
        fse.write_db()
        return "Removed Feide service '{}'".format(service_name)

    #
    # feide service_list
    #
    all_commands['feide_service_list'] = Command(
        ('feide', 'service_list'),
        fs=FormatSuggestion(
            '%-12d %-12d %s', ('service_id', 'feide_id', 'name'),
            hdr='%-12s %-12s %s' % ('Entity ID', 'Feide ID', 'Name')),
        perm_filter='is_superuser')

    def feide_service_list(self, operator):
        """ List Feide services. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superusers may list Feide services')
        fse = FeideService(self.db)
        return map(dict, fse.search())

    #
    # feide authn_level_add
    #
    all_commands['feide_authn_level_add'] = Command(
        ('feide', 'authn_level_add'),
        SimpleString(help_ref='feide_service_name'),
        SimpleString(help_ref='feide_authn_entity_target'),
        Integer(help_ref='feide_authn_level'),
        perm_filter='is_superuser')

    def feide_authn_level_add(self, operator, service_name, target, level):
        """ Add an authentication level for a given service and entity. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may add Feide authentication levels')
        if not level.isdigit() or int(level) not in (3, 4):
            raise CerebrumError('Authentication level must be 3 or 4')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        # Allow authentication levels for persons and groups
        entity = self.util.get_target(target,
                                      default_lookup='person',
                                      restrict_to=['Person', 'Group'])
        if entity.search_authn_level(service_id=fse.entity_id,
                                     entity_id=entity.entity_id,
                                     level=level):
            raise CerebrumError(
                'Authentication level {} for {} for service {} '
                'already enabled'.format(level, target, service_name))
        entity.add_authn_level(service_id=fse.entity_id, level=level)
        return 'Added authentication level {} for {} for {}'.format(
            level, target, service_name)

    #
    # feide authn_level_remove
    #
    all_commands['feide_authn_level_remove'] = Command(
        ('feide', 'authn_level_remove'),
        SimpleString(help_ref='feide_service_name'),
        SimpleString(help_ref='feide_authn_entity_target'),
        Integer(help_ref='feide_authn_level'),
        perm_filter='is_superuser')

    def feide_authn_level_remove(self, operator, service_name, target, level):
        """ Remove an authentication level for a given service and entity. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may remove Feide authentication levels')
        if not level.isdigit() or int(level) not in (3, 4):
            raise CerebrumError('Authentication level must be 3 or 4')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        # Allow authentication levels for persons and groups
        entity = self.util.get_target(target,
                                      default_lookup='person',
                                      restrict_to=['Person', 'Group'])
        if not entity.search_authn_level(service_id=fse.entity_id,
                                         entity_id=entity.entity_id,
                                         level=level):
            raise CerebrumError(
                'No such authentication level {} for {} for service {}'.format(
                    level, target, service_name))
        entity.remove_authn_level(service_id=fse.entity_id, level=level)
        return 'Removed authentication level {} for {} for {}'.format(
            level, target, service_name)

    #
    # feide authn_level_search
    #
    all_commands['feide_authn_level_list'] = Command(
        ('feide', 'authn_level_list'),
        SimpleString(help_ref='feide_service_name'),
        fs=FormatSuggestion(
            '%-20s %-6d %s', ('service_name', 'level', 'entity'),
            hdr='%-20s %-6s %s' % ('Service', 'Level', 'Entity')),
        perm_filter='is_superuser')

    def feide_authn_level_list(self, operator, service_name):
        """ List all authentication levels for a service. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied(
                'Only superusers may list Feide authentication levels')
        service_name = service_name.strip()
        fse = self._find_service(service_name)
        en = Factory.get('Entity')(self.db)
        fsal = FeideServiceAuthnLevelMixin(self.db)
        result = []
        for x in fsal.search_authn_level(service_id=fse.entity_id):
            try:
                en.clear()
                en.find(x['entity_id'])
                entity_type = six.text_type(
                    self.const.map_const(en.entity_type))
                entity_name = self._get_entity_name(en.entity_id,
                                                    en.entity_type)
                entity = '{} {} (id:{:d})'.format(entity_type, entity_name,
                                                  en.entity_id)
            except:
                entity = 'id:{}'.format(x['entity_id'])
            result.append({
                'service_name': service_name,
                'level': x['level'],
                'entity': entity
            })
        return result
示例#17
0
class BofhdExtension(BofhdCommandBase):
    """The BofhdExctension for AD related commands and functionality."""

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'ad': "Commands for AD related functionality",
            }

        # The texts in command_help are automatically line-wrapped, and should
        # not contain \n
        command_help = {
            'ad': {
                'ad_attributetypes':
                    'List out all defined AD-attribute types',
                'ad_list_attributes':
                    'List all attributes related to given spread',
                'ad_info':
                    'Get AD-related information about an entity',
                'ad_set_attribute':
                    'Set a given AD-attribute for an entity',
                'ad_remove_attribute':
                    'Remove a given AD-attribute for an entity',
                },
            }

        arg_help = {
            'attr_type':
                ['attr_type', 'Type of AD-attribute',
                 'See "ad attributetypes" for a list of defined types'],
            'attr_value':
                ['value', 'Value of AD-attribute',
                 'A string value for the AD-attribute'],
            'id':
                ['id', 'Id',
                 'The identity of an entity'],
            'spread':
                ['spread', 'Spread',
                 'The spread for the attribute'],
            }
        return (group_help, command_help, arg_help)

    # ad_attributetypes
    all_commands['ad_attributetypes'] = Command(
        ("ad", "attributetypes"),
        fs=FormatSuggestion(
            "%-14s %-8s %s", ('name', 'multi', 'desc'),
            hdr="%-14s %-8s %s" % ('Name', 'Multival', 'Description')
        )
    )

    def ad_attributetypes(self, operator):
        """List out all types of AD-attributes defined in Cerebrum."""
        return [
            {
                'name': six.text_type(c),
                'multi': c.multivalued,
                'desc': c.description,
            }
            for c in self.const.fetch_constants(self.const.ADAttribute)
        ]

    #
    # ad_list_attributes
    #
    all_commands['ad_list_attributes'] = Command(
        ("ad", "list_attributes"),
        AttributeType(optional=True),
        Spread(optional=True),
        fs=FormatSuggestion(
            "%-20s %-20s %-20s %s", ('attr_type', 'spread', 'entity', 'value'),
            hdr="%-20s %-20s %-20s %s" % ('Attribute', 'Spread', 'Entity',
                                          'Value')
        ),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_list_attributes(self, operator, attr_type=None, spread=None):
        """List all attributes, limited to given input."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        # TODO: check if operator has access to the entity

        atr = spr = None
        if attr_type:
            atr = _get_attr(self.const, attr_type)
        if spread:
            spr = _get_spread(self.const, spread)

        ent = EntityADMixin(self.db)
        return [
            {
                'attr_type': six.text_type(
                    self.const.ADAttribute(row['attr_code'])),
                'spread': six.text_type(
                    self.const.Spread(row['spread_code'])),
                'entity': self._get_entity_name(row['entity_id']),
                'value': row['value'],
            }
            for row in ent.list_ad_attributes(spread=spr, attribute=atr)
        ]

    #
    # ad_info
    #
    all_commands['ad_info'] = Command(
        ("ad", "info"),
        EntityType(),
        Id(),
        fs=FormatSuggestion([
            ('AD-id: %-12s %s', ('id_type', 'ad_id')),
            ('%-20s %-20s %s', ('spread', 'attr_type', 'attr_value'),
             '%-20s %-20s %s' % ('Spread', 'Attribute', 'Value')),
        ]),
        perm_filter='is_superuser'
    )

    def ad_info(self, operator, entity_type, ident):
        """Return AD related information about a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        ent = self._get_entity(entity_type, ident)
        # TODO: check if operator has access to the entity?
        ret = []
        for row in ent.search_external_ids(
                source_system=self.const.system_ad,
                entity_id=ent.entity_id, fetchall=False):
            ret.append({
                'id_type': six.text_type(
                    self.const.EntityExternalId(row['id_type'])),
                'ad_id': row['external_id'],
            })
        for row in ent.list_ad_attributes(entity_id=ent.entity_id):
            ret.append({
                'attr_type': six.text_type(
                    self.const.ADAttribute(row['attr_code'])),
                'spread': six.text_type(
                    self.const.Spread(row['spread_code'])),
                'attr_value': row['value'],
            })
        return ret

    #
    # ad_set_attribute <entity-type> <entity-id> <attr> <spread> <value>
    #
    all_commands['ad_set_attribute'] = Command(
        ("ad", "set_attribute"),
        EntityType(),
        Id(),
        AttributeType(),
        Spread(),
        SimpleString(help_ref='attr_value'),
        fs=FormatSuggestion([
            ("AD-attribute %s set for %s, limited to spread %s: %s",
             ('attribute', 'entity_name', 'spread', 'value')),
            ('WARNING: %s', ('warning', ))
        ]),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_set_attribute(self, operator,
                         entity_type, ident, attr_type, spread, value):
        """Set an attribute for a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        # TODO: check if operator has access to the entity
        ent = self._get_entity(entity_type, ident)
        atr = _get_attr(self.const, attr_type)
        spr = _get_spread(self.const, spread)
        ent.set_ad_attribute(spread=spr, attribute=atr, value=value)
        ent.write_db()

        # We keep using the constant strvals:
        spread = six.text_type(spr)
        attr_type = six.text_type(atr)

        # Check if the spread and attribute is defined for an AD-sync. If not,
        # add a warning to the output.

        retval = [{
            'attribute': attr_type,
            'entity_name': self._get_entity_name(ent.entity_id),
            'entity_type': six.text_type(ent.entity_type),
            'spread': spread,
            'value': value,
        }]

        config = getattr(cereconf, 'AD_SPREADS', None)
        if config:
            if spread not in config:
                retval.append({
                    'warning': 'No AD-sync defined for spread: %s' % spread,
                })
            elif attr_type not in config[spread].get('attributes', ()):
                retval.append({
                    'warning': 'AD-sync for %s does not know of: %s' % (
                        attr_type, spread),
                })

        return retval

    #
    # ad_remove_attribute <entity-type> <entity-id> <attr> <spread>
    #
    all_commands['ad_remove_attribute'] = Command(
        ("ad", "remove_attribute"),
        EntityType(),
        Id(),
        AttributeType(),
        Spread(),
        fs=FormatSuggestion(
            "AD-attribute %s removed for %s, for spread %s",
            ('attribute', 'entity_name', 'spread')
        ),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_remove_attribute(self, operator,
                            entity_type, id, attr_type, spread):
        """Remove an AD-attribute for a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        ent = self._get_entity(entity_type, id)
        atr = _get_attr(self.const, attr_type)
        spr = _get_spread(self.const, spread)
        try:
            ent.delete_ad_attribute(spread=spr, attribute=atr)
        except Errors.NotFoundError:
            raise CerebrumError(
                '%s does not have AD-attribute %s with spread %s' %
                (ent.entity_id,
                 six.text_type(atr),
                 six.text_type(spr))
            )
        ent.write_db()

        return {
            'attribute': six.text_type(atr),
            'entity_name': self._get_entity_name(ent.entity_id),
            'entity_type': six.text_type(ent.entity_type),
            'spread': six.text_type(spr),
        }
示例#18
0
class BofhdEmailSympaMixin(BofhdEmailListMixinBase):
    """ Email list related commands for the Sympa mailing list system. """

    default_sympa_commands = {}

    # TODO: Move all sympa specific methods and attributes here.

    # SYMPA SETTINGS
    #
    # aliases that we must create for each sympa mailing list.
    # -request, -editor, -owner, -subscribe, -unsubscribe all come from sympa
    # owner- and -admin are the remnants of mailman
    _sympa_addr2alias = (
        # The first one *is* the official/primary name. Don't reshuffle.
        ('%(local_part)s@%(domain)s', '|SYMPA_QUEUE %(listname)s'),
        # Owner addresses...
        ('%(local_part)s-owner@%(domain)s', '|SYMPA_BOUNCEQUEUE %(listname)s'),
        ('%(local_part)s-admin@%(domain)s', '|SYMPA_BOUNCEQUEUE %(listname)s'),
        # Request addresses...
        ('%(local_part)s-request@%(domain)s',
         '|SYMPA_QUEUE %(local_part)s-request@%(domain)s'),
        ('owner-%(local_part)s@%(domain)s',
         '|SYMPA_QUEUE %(local_part)s-request@%(domain)s'),
        # Editor address...
        ('%(local_part)s-editor@%(domain)s',
         '|SYMPA_QUEUE %(local_part)s-editor@%(domain)s'),
        # Subscribe address...
        ('%(local_part)s-subscribe@%(domain)s',
         '|SYMPA_QUEUE %(local_part)s-subscribe@%(domain)s'),
        # Unsubscribe address...
        ('%(local_part)s-unsubscribe@%(domain)s',
         '|SYMPA_QUEUE %(local_part)s-unsubscribe@%(domain)s'),
    )
    _sympa_address_suffixes = (
        '-owner',
        '-admin',
        '-request',
        '-editor',
        '-subscribe',
        '-unsubscribe',
    )
    _sympa_address_prefixes = ('owner-', )

    def _validate_sympa_list(self, listname):
        """ Check whether L{listname} is the 'official' name for a sympa ML.

        Raises CerebrumError if it is not.

        """
        if self._get_sympa_list(listname) != listname:
            raise CerebrumError("%s is NOT the official Sympa list name" %
                                listname)
        return listname

    def _get_sympa_list(self, listname):
        """ Try to return the 'official' sympa mailing list name, if it can at
        all be derived from listname.

        The problem here is that some lists are actually called
        foo-admin@domain (and their admin address is foo-admin-admin@domain).

        Since the 'official' names are not tagged in any way, we try to
        guess. The guesswork proceeds as follows:

        1) if listname points to a sympa ET that has a primary address, we are
           done, listname *IS* the official list name
        2) if not, then there must be a prefix/suffix (like -request) and if
           we chop it off, we can checked the chopped off part for being an
           official sympa list. The chopping off continues until we run out of
           special prefixes/suffixes.
        """

        ea = Email.EmailAddress(self.db)
        et = Email.EmailTarget(self.db)
        epat = Email.EmailPrimaryAddressTarget(self.db)

        def has_prefix(address):
            local_part, domain = self._split_email_address(address)
            return True in [
                local_part.startswith(x) for x in self._sympa_address_prefixes
            ]

        def has_suffix(address):
            local_part, domain = self._split_email_address(address)
            return True in [
                local_part.endswith(x) for x in self._sympa_address_suffixes
            ]

        def has_primary_to_me(address):
            try:
                ea.clear()
                ea.find_by_address(address)
                epat.clear()
                epat.find(ea.get_target_id())
                return True
            except Errors.NotFoundError:
                return False

        def I_am_sympa(address, check_suffix_prefix=True):
            try:
                ea.clear()
                ea.find_by_address(address)
            except Errors.NotFoundError:
                # If it does not exist, it cannot be sympa
                return False

            et.clear()
            et.find(ea.get_target_id())
            if ((not et.email_target_alias)
                    or et.email_target_type != self.const.email_target_Sympa):
                # if it's not a Sympa ET, address cannot be sympa
                return False
            return True

        not_sympa_error = CerebrumError("%s is not a Sympa list" % listname)
        # Simplest case -- listname is actually a sympa ML directly. It does
        # not matter whether it has a funky prefix/suffix.
        if I_am_sympa(listname) and has_primary_to_me(listname):
            return listname

        # However, if listname does not have a prefix/suffix AND it is not a
        # sympa address with a primary address, them it CANNOT be a sympa
        # address.
        if not (has_prefix(listname) or has_suffix(listname)):
            raise not_sympa_error

        # There is a funky suffix/prefix. Is listname actually such a
        # secondary address? Try to chop off the funky part and test.
        local_part, domain = self._split_email_address(listname)
        for prefix in self._sympa_address_prefixes:
            if not local_part.startswith(prefix):
                continue

            lp_tmp = local_part[len(prefix):]
            addr_to_test = lp_tmp + "@" + domain
            try:
                self._get_sympa_list(addr_to_test)
                return addr_to_test
            except CerebrumError:
                pass

        for suffix in self._sympa_address_suffixes:
            if not local_part.endswith(suffix):
                continue

            lp_tmp = local_part[:-len(suffix)]
            addr_to_test = lp_tmp + "@" + domain
            try:
                self._get_sympa_list(addr_to_test)
                return addr_to_test
            except CerebrumError:
                pass

        raise not_sympa_error

    def _is_mailing_list(self, listname):
        """ Check whether L{listname} refers to a valid mailing list.

        :rtype: bool
        :return: True if listname points to a valid Sympa list, else False.

        """
        # Sympa list?
        try:
            self._validate_sympa_list(listname)
            return True
        except CerebrumError:
            pass

        # Must call super() so that all mixins are checked
        if super(BofhdEmailSympaMixin, self)._is_mailing_list(listname):
            return True

        return False

    def _get_all_related_maillist_targets(self, address):
        """ Locate and return all email targets (addresses) for a sympa list.

        Given any address associated with a mailing list, this method returns
        all the EmailTarget id's that are associated with that mailing list.

        See method in superclass for more info.

        """
        et, ea = self._get_email_target_and_address(address)

        if et.email_target_type != self.const.email_target_Sympa:
            # Not Sympa list, let the superclass deal with it.
            return super(BofhdEmailSympaMixin,
                         self)._get_all_related_maillist_targets(address)

        official_ml_address = self._get_sympa_list(ea.get_address())
        patterns = [x[0] for x in self._sympa_addr2alias]

        return self._get_maillist_targets_for_pattern(official_ml_address,
                                                      patterns)

    def _create_sympa_list(self,
                           operator,
                           listname,
                           delivery_host,
                           force=False):
        """ Create a new Sympa list in cerebrum. """
        local_part, domain = self._request_list_localpart_domain(operator,
                                                                 listname,
                                                                 force=force)
        self._register_sympa_list_addresses(listname, local_part, domain,
                                            delivery_host)
        # register auto spam and filter settings for the list
        self._register_spam_settings(listname, self.const.email_target_Sympa)
        self._register_filter_settings(listname, self.const.email_target_Sympa)

    def _create_sympa_list_alias(self,
                                 operator,
                                 listname,
                                 address,
                                 delivery_host,
                                 force_alias=False):
        """Create an alias L{address} for an existing Sympa L{listname}.

        @type listname: basestring
        @param listname:
          Email address for an existing mailing list. This is the ML we are
          aliasing.

        @type address: basestring
        @param address:
          Email address which will be the alias.

        @type delivery_host: EmailServer instance or None.
        @param delivery_host: Host where delivery to the mail alias happens.

        """
        lp, dom = self._split_email_address(address)
        ed = self._get_email_domain_from_str(dom)
        self.ba.can_email_list_create(operator.get_entity_id(), ed)
        self._validate_sympa_list(listname)

        if not force_alias:
            try:
                self._get_account(lp, idtype='name')
            except CerebrumError:
                pass
            else:
                raise CerebrumError(
                    "Won't create list-alias %s, beause %s is a username" %
                    (address, lp))

        # we _don't_ check for "more than 8 characters in local
        # part OR it contains hyphen" since we assume the people
        # who have access to this command know what they are doing
        self._register_sympa_list_addresses(listname, lp, dom, delivery_host)

    def _register_sympa_list_addresses(self, listname, local_part, domain,
                                       delivery_host):
        """ Register all neccessary sympa addresses.

        Add list, request, editor, owner, subscribe and unsubscribe addresses
        to a sympa mailing list.

        @type listname: basestring
        @param listname:
            Sympa listname that the operation is about. listname is typically
            different from local_part@domain when we are creating an alias.
            local_part@domain is the alias, listname is the original listname.
            And since aliases should point to the 'original' ETs, we have to
            use listname to locate the ETs.

        @type local_part: basestring
        @param local_part: See domain

        @type domain: basestring
        @param domain:
            L{local_part} and domain together represent a new list address that
            we want to create.

        @type delivery_host: EmailServer instance.
        @param delivery_host:
            EmailServer where e-mail to L{listname} is to be delivered through.

        """

        if (delivery_host.email_server_type !=
                self.const.email_server_type_sympa):
            raise CerebrumError(
                "Delivery host %s has wrong type (%s) for sympa list %s" %
                (delivery_host.get_name(self.const.host_namespace),
                 self.const.EmailServerType(
                     delivery_host.email_server_type), listname))

        ed = Email.EmailDomain(self.db)
        ed.find_by_domain(domain)

        et = Email.EmailTarget(self.db)
        ea = Email.EmailAddress(self.db)
        epat = Email.EmailPrimaryAddressTarget(self.db)
        try:
            ea.find_by_local_part_and_domain(local_part, ed.entity_id)
        except Errors.NotFoundError:
            pass
        else:
            raise CerebrumError("The address %s@%s is already in use" %
                                (local_part, domain))

        sympa = self._get_account('sympa', idtype='name', actype='PosixUser')
        primary_ea_created = False
        listname_lp, listname_domain = listname.split("@")

        # For each of the addresses we are supposed to create...
        for pattern, pipe_destination in self._sympa_addr2alias:
            address = pattern % locals()
            address_lp, address_domain = address.split("@")

            # pipe has to be derived from the original listname, since it's
            # used to locate the ET.
            pipe = pipe_destination % {
                'local_part': listname_lp,
                'domain': listname_domain,
                'listname': listname
            }

            # First check whether the address already exist. It should not.
            try:
                ea.clear()
                ea.find_by_local_part_and_domain(address_lp, ed.entity_id)
                raise CerebrumError("Can't add list %s as the address %s "
                                    "is already in use" % (listname, address))
            except Errors.NotFoundError:
                pass

            # Then find the target for this particular email address. The
            # target may already exist, though.
            et.clear()
            try:
                et.find_by_alias_and_account(pipe, sympa.entity_id)
            except Errors.NotFoundError:
                et.populate(self.const.email_target_Sympa,
                            alias=pipe,
                            using_uid=sympa.entity_id,
                            server_id=delivery_host.entity_id)
                et.write_db()

            # Then create the email address and associate it with the ET.
            ea.clear()
            ea.populate(address_lp, ed.entity_id, et.entity_id)
            ea.write_db()

            # And finally, the primary address. The first entry in
            # _sympa_addr2alias will match. Do not reshuffle that tuple!
            if not primary_ea_created:
                epat.clear()
                try:
                    epat.find(et.entity_id)
                except Errors.NotFoundError:
                    epat.clear()
                    epat.populate(ea.entity_id, parent=et)
                    epat.write_db()
                primary_ea_created = True

    def _email_info_sympa(self, operator, et, addr):
        """ Collect Sympa-specific information for a ML L{addr}. """
        def fish_information(suffix, local_part, domain, listname):
            """Generate an entry for sympa info for the specified address.

            @type address: basestring
            @param address:
              Is the address we are looking for (we locate ETs based on the
              alias value in _sympa_addr2alias).
            @type et: EmailTarget instance

            @rtype: sequence (of dicts of basestring to basestring)
            @return:
              A sequence of dicts suitable for merging into return value from
              email_info_sympa.
            """

            result = []
            address = "%(local_part)s-%(suffix)s@%(domain)s" % locals()
            target_alias = None
            for a, alias in self._sympa_addr2alias:
                a = a % locals()
                if a == address:
                    target_alias = alias % locals()
                    break

            # IVR 2008-08-05 TBD Is this an error? All sympa ETs must have an
            # alias in email_target.
            if target_alias is None:
                return result

            try:
                # Do NOT change et's (parameter's) state.
                et_tmp = Email.EmailTarget(self.db)
                et_tmp.clear()
                et_tmp.find_by_alias(target_alias)
            except Errors.NotFoundError:
                return result

            addrs = et_tmp.get_addresses()
            if not addrs:
                return result

            pattern = '%(local_part)s@%(domain)s'
            result.append({'sympa_' + suffix + '_1': pattern % addrs[0]})
            for idx in range(1, len(addrs)):
                result.append({'sympa_' + suffix: pattern % addrs[idx]})
            return result

        # end fish_information

        # listname may be one of the secondary addresses.
        # email info sympatest@domain MUST be equivalent to
        # email info sympatest-admin@domain.
        listname = self._get_sympa_list(addr)
        ret = [{"sympa_list": listname}]
        if listname.count('@') == 0:
            lp, dom = listname, addr.split('@')[1]
        else:
            lp, dom = listname.split('@')

        ed = Email.EmailDomain(self.db)
        ed.find_by_domain(dom)
        ea = Email.EmailAddress(self.db)
        try:
            ea.find_by_local_part_and_domain(lp, ed.entity_id)
        except Errors.NotFoundError:
            raise CerebrumError(
                "Address %s exists, but the list it points to, %s, does not" %
                (addr, listname))
        # now find all e-mail addresses
        et_sympa = Email.EmailTarget(self.db)
        et_sympa.clear()
        et_sympa.find(ea.email_addr_target_id)
        addrs = self._get_valid_email_addrs(et_sympa, sort=True)
        # IVR 2008-08-21 According to postmasters, only superusers should see
        # forwarding and delivery host information
        if self.ba.is_postmaster(operator.get_entity_id()):
            if et_sympa.email_server_id is None:
                delivery_host = "N/A (this is an error)"
            else:
                delivery_host = self._get_email_server(
                    et_sympa.email_server_id).name
            ret.append({"sympa_delivery_host": delivery_host})
        ret += self._email_info_forwarding(et_sympa, addrs)
        aliases = []
        for row in et_sympa.get_addresses():
            a = "%(local_part)s@%(domain)s" % row
            if a == listname:
                continue
            aliases.append(a)
        if aliases:
            ret.append({"sympa_alias_1": aliases[0]})
        for next_alias in aliases[1:]:
            ret.append({"sympa_alias": next_alias})

        for suffix in ("owner", "request", "editor", "subscribe",
                       "unsubscribe"):
            ret.extend(fish_information(suffix, lp, dom, listname))

        return ret

    # COMMANDS

    #
    # sympa create_list <run-host> <delivery-host> <listaddr> <admins>
    #                   <profile> <desc> [force?]
    #
    default_sympa_commands['sympa_create_list'] = Command(
        ("sympa", "create_list"),
        SimpleString(help_ref='string_exec_host'),
        SimpleString(help_ref='string_email_delivery_host'),
        EmailAddress(help_ref="mailing_list"),
        SimpleString(help_ref="mailing_admins"),
        SimpleString(help_ref="mailing_list_profile"),
        SimpleString(help_ref="mailing_list_description"),
        YesNo(help_ref="yes_no_force", optional=True, default="No"),
        fs=FormatSuggestion([("Sympa list '%s' created", ('listname', ))]),
        perm_filter="can_email_list_create")

    def sympa_create_list(self,
                          operator,
                          run_host,
                          delivery_host,
                          listname,
                          admins,
                          list_profile,
                          list_description,
                          yes_no_force="No"):
        """ Create a sympa list in Cerebrum and on the sympa server(s).

        Registers all the necessary cerebrum information and make a bofhd
        request for the actual list creation.

        """
        # Check that the profile is legal
        if list_profile not in cereconf.SYMPA_PROFILES:
            raise CerebrumError("Profile %s for sympa list %s is not valid" %
                                (list_profile, listname))

        # Check that the command exec host is sane
        if run_host not in cereconf.SYMPA_RUN_HOSTS:
            raise CerebrumError("run-host '%s' for list '%s' is not valid" %
                                (run_host, listname))

        metachars = "'\"$&()*;<>?[\\]`{|}~\n"

        def has_meta(s1, s2=metachars):
            """Check if any char of s1 is in s2"""
            for c in s1:
                if c in s2:
                    return True
            return False

        # Sympa list creation command will be passed through multiple
        # exec/shells. Better be restrictive.
        if True in [
                has_meta(x) for x in (run_host, delivery_host, listname,
                                      admins, list_profile, list_description)
        ]:
            raise CerebrumError(
                "Illegal metacharacter in list parameter. Allowed: '%s'" %
                metachars)

        delivery_host = self._get_email_server(delivery_host)
        force = self._is_yes(yes_no_force)
        self._create_sympa_list(operator, listname, delivery_host, force=force)
        # Now make a bofhd request to create the list itself
        admin_list = list()
        for item in admins.split(","):
            # it's a user name. That username must exist in Cerebrum
            if "@" not in item:
                self._get_account(item)
                # TODO: Not good, this is in use by UIA
                item = item + "@ulrik.uio.no"
            admin_list.append(item)

        # Make the request.
        lp, dom = self._split_email_address(listname)
        ed = self._get_email_domain_from_str(dom)
        ea = Email.EmailAddress(self.db)
        ea.clear()
        ea.find_by_local_part_and_domain(lp, ed.entity_id)
        list_id = ea.entity_id
        # IVR 2008-08-01 TBD: this is a big ugly. We need to pass several
        # arguments to p_b_r, but we cannot really store them anywhere :( The
        # idea is then to take a small dict, pickle it, shove into state_data,
        # unpickle in p_b_r and be on our merry way. It is at the very best
        # suboptimal.
        state = {
            "runhost": run_host,  # IVR 2008-08-01 FIXME: non-fqdn? force?
            # check?
            "admins": admin_list,
            "profile": list_profile,
            "description": list_description,
        }
        br = BofhdRequests(self.db, self.const)

        # IVR 2009-04-17 +30 minute delay to allow changes to spread to
        # LDAP. The postmasters are nagging for that delay. All questions
        # should be directed to them (this is similar to delaying a delete
        # request).
        br.add_request(operator.get_entity_id(),
                       DateTime.now() + DateTime.DateTimeDelta(0, 0, 30),
                       self.const.bofh_sympa_create,
                       list_id,
                       ea.entity_id,
                       state_data=pickle.dumps(state))
        return {'listname': listname}

    #
    # sympa delete_list <run-host> <list-address>
    #
    default_sympa_commands['sympa_remove_list'] = Command(
        ("sympa", "remove_list"),
        SimpleString(help_ref='string_exec_host'),
        EmailAddress(help_ref="mailing_list_exist"),
        YesNo(help_ref="yes_no_with_request"),
        fs=FormatSuggestion([("Sympa list '%s' deleted (bofhd request: %s)", (
            'listname',
            'request',
        ))]),
        perm_filter="can_email_list_delete")

    def sympa_remove_list(self, operator, run_host, listname, force_yes_no):
        """ Remove a sympa list from cerebrum.

        @type force_request: bool
        @param force_request:
          Controls whether a bofhd request should be issued. This may come in
          handy, if we want to delete a sympa list from Cerebrum only and not
          issue any requests. misc cancel_request would have worked too, but
          it's better to merge this into one command.

        """
        force_request = self._is_yes(force_yes_no)

        # Check that the command exec host is sane
        if run_host not in cereconf.SYMPA_RUN_HOSTS:
            raise CerebrumError("run-host '%s' for list '%s' is not valid" %
                                (run_host, listname))

        et, ea = self._get_email_target_and_address(listname)
        self.ba.can_email_list_delete(operator.get_entity_id(), ea)

        if et.email_target_type != self.const.email_target_Sympa:
            raise CerebrumError(
                "'%s' is not a sympa list (type: %s)" %
                (listname, self.const.EmailTarget(et.email_target_type)))

        epat = Email.EmailPrimaryAddressTarget(self.db)
        list_id = ea.entity_id
        # Now, there are *many* ETs/EAs associated with one sympa list. We
        # have to wipe them all out.
        if not self._validate_sympa_list(listname):
            raise CerebrumError("Illegal sympa list name: '%s'", listname)

        deleted_EA = self.email_info(operator, listname)
        # needed for pattern interpolation below (these are actually used)
        local_part, domain = self._split_email_address(listname)
        for pattern, pipe_destination in self._sympa_addr2alias:
            address = pattern % locals()
            # For each address, find the target, and remove all email
            # addresses for that target (there may be many addresses for the
            # same target).
            try:
                ea.clear()
                ea.find_by_address(address)
                et.clear()
                et.find(ea.get_target_id())
                epat.clear()
                try:
                    epat.find(et.entity_id)
                except Errors.NotFoundError:
                    pass
                else:
                    epat.delete()
                # Wipe all addresses...
                for row in et.get_addresses():
                    addr = '%(local_part)s@%(domain)s' % row
                    ea.clear()
                    ea.find_by_address(addr)
                    ea.delete()
                et.delete()
            except Errors.NotFoundError:
                pass

        if cereconf.INSTITUTION_DOMAIN_NAME == 'uio.no':
            self._report_deleted_EA(deleted_EA)
        if not force_request:
            return {'listname': listname, 'request': False}

        br = BofhdRequests(self.db, self.const)
        state = {'run_host': run_host, 'listname': listname}
        br.add_request(
            operator.get_entity_id(),
            # IVR 2008-08-04 +1 hour to allow changes to spread to
            # LDAP. This way we'll have a nice SMTP-error, rather
            # than a confusing error burp from sympa.
            DateTime.now() + DateTime.DateTimeDelta(0, 1),
            self.const.bofh_sympa_remove,
            list_id,
            None,
            state_data=pickle.dumps(state))

        return {'listname': listname, 'request': True}

    #
    # sympa create_list_alias <list-address> <new-alias>
    #
    default_sympa_commands['sympa_create_list_alias'] = Command(
        ("sympa", "create_list_alias"),
        EmailAddress(help_ref="mailing_list_exist"),
        EmailAddress(help_ref="mailing_list"),
        YesNo(help_ref="yes_no_force", optional=True),
        fs=FormatSuggestion([("List alias '%s' created", ('alias', ))]),
        perm_filter="can_email_list_create")

    def sympa_create_list_alias(self,
                                operator,
                                listname,
                                address,
                                yes_no_force='No'):
        """ Create a secondary name for an existing Sympa list. """
        force = self._is_yes(yes_no_force)
        # The first thing we have to do is to locate the delivery
        # host. Postmasters do NOT want to allow people to specify a different
        # delivery host for alias than for the list that is being aliased. So,
        # find the ml's ET and fish out the server_id.
        self._validate_sympa_list(listname)
        local_part, domain = self._split_email_address(listname)
        ed = self._get_email_domain_from_str(domain)
        email_address = Email.EmailAddress(self.db)
        email_address.find_by_local_part_and_domain(local_part, ed.entity_id)
        try:
            delivery_host = self._get_server_from_address(email_address)
        except CerebrumError:
            raise CerebrumError("Cannot alias list %s (missing delivery host)",
                                listname)

        # TODO: Look at perms (are now done by _register)
        self._create_sympa_list_alias(operator,
                                      listname,
                                      address,
                                      delivery_host,
                                      force_alias=force)
        return {
            'target': listname,
            'alias': address,
        }

    #
    # email remove_sympa_list_alias <alias>
    #
    default_sympa_commands['sympa_remove_list_alias'] = Command(
        ('sympa', 'remove_list_alias'),
        EmailAddress(help_ref='mailing_list_alias'),
        fs=FormatSuggestion([("List alias '%s' removed", ('alias', ))]),
        perm_filter='can_email_list_create')

    def sympa_remove_list_alias(self, operator, alias):
        """ Remove Sympa list aliases. """
        lp, dom = self._split_email_address(alias, with_checks=False)
        ed = self._get_email_domain_from_str(dom)
        self.ba.can_email_list_create(operator.get_entity_id(), ed)

        ea = Email.EmailAddress(self.db)
        et = Email.EmailTarget(self.db)

        for addr_format, pipe in self._sympa_addr2alias:
            addr = addr_format % {
                "local_part": lp,
                "domain": dom,
            }
            try:
                ea.clear()
                ea.find_by_address(addr)
            except Errors.NotFoundError:
                # Even if one of the addresses is missing, it does not matter
                # -- we are removing the alias anyway. The right thing to do
                # here is to continue, as if deletion worked fine. Note that
                # the ET belongs to the original address, not the alias, so if
                # we don't delete it when the *alias* is removed, we should
                # still be fine.
                continue

            try:
                et.clear()
                et.find(ea.email_addr_target_id)
            except Errors.NotFoundError:
                raise CerebrumError("Could not find e-mail target for %s" %
                                    addr)

            # nuke the address, and, if it's the last one, nuke the target as
            # well.
            self._remove_email_address(et, addr)
        return {'alias': alias}

    #
    # email_create_sympa_cerebrum_list
    # TODO: WTF?
    #
    default_sympa_commands['sympa_create_list_in_cerebrum'] = Command(
        ("sympa", "create_list_in_cerebrum"),
        SimpleString(help_ref='string_email_delivery_host'),
        EmailAddress(help_ref="mailing_list"),
        YesNo(help_ref="yes_no_force", optional=True, default="No"),
        fs=FormatSuggestion([("Sympa list '%s' created (only in Cerebrum)",
                              ('listname', ))]),
        perm_filter="can_email_list_create")

    def sympa_create_list_in_cerebrum(self,
                                      operator,
                                      delivery_host,
                                      listname,
                                      yes_no_force=None):
        """ Create a sympa mailing list in cerebrum only. """

        delivery_host = self._get_email_server(delivery_host)
        force = self._is_yes(yes_no_force)
        self._create_sympa_list(operator, listname, delivery_host, force=force)
        return {'listname': listname}
class BofhdContactCommands(BofhdCommandBase):

    all_commands = {}
    authz = BofhdContactAuth

    @property
    def util(self):
        # TODO: Or should we inherit from BofhdCommonMethods?
        #       We're not really interested in user_delete, etc...
        try:
            return self.__util
        except AttributeError:
            self.__util = BofhdUtils(self.db)
            return self.__util

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        # look up types
        co = Factory.get('Constants')()
        source_systems = co.fetch_constants(co.AuthoritativeSystem)
        contact_types = co.fetch_constants(co.ContactInfo)

        # Enrich cmd_args with actual constants.
        # TODO: Find a better way to do this for all similar cmd_args
        cmd_args = {}
        list_sep = '\n - '
        for k, v in CMD_ARGS.items():
            cmd_args[k] = v[:]
            if k == 'entity_contact_source_system':
                cmd_args[k][2] += '\nSource systems:'
                cmd_args[k][2] += list_sep + list_sep.join(six.text_type(c) for
                                                           c in source_systems)
            if k == 'entity_contact_type':
                cmd_args[k][2] += '\nContact types:'
                cmd_args[k][2] += list_sep + list_sep.join(six.text_type(c) for
                                                           c in contact_types)
        del co

        return merge_help_strings(
            ({}, {}, cmd_args),  # We want _our_ cmd_args to win!
            get_help_strings(),
            ({}, CMD_HELP, {}))

    #
    # entity contactinfo_add <entity> <contact type> <contact value>
    #
    all_commands['entity_contactinfo_add'] = Command(
        ('entity', 'contactinfo_add'),
        SimpleString(help_ref='id:target:entity'),
        SimpleString(help_ref='entity_contact_type'),
        SimpleString(help_ref='entity_contact_value'),
        fs=FormatSuggestion(
            "Added contact info %s:%s '%s' to '%s' with id=%d",
            ('source_system', 'contact_type', 'contact_value', 'entity_type',
             'entity_id')
        ),
        perm_filter='can_add_contact_info',
    )

    def entity_contactinfo_add(self, operator,
                               entity_target, contact_type, contact_value):
        """Manually add contact info to an entity."""
        # default values
        contact_pref = 50
        source_system = self.const.system_manual
        contact_type = contact_type.upper()
        # get entity object
        entity = self.util.get_target(entity_target, restrict_to=[])
        entity_type = self.const.EntityType(int(entity.entity_type))

        if not hasattr(entity, 'get_contact_info'):
            raise CerebrumError("No support for contact info in %s entity" %
                                six.text_type(entity_type))

        # validate contact info type
        contact_type = self._get_constant(self.const.ContactInfo,
                                          contact_type)

        # check permissions
        self.ba.can_add_contact_info(operator.get_entity_id(),
                                     entity=entity,
                                     contact_type=contact_type)

        # validate email
        if contact_type == self.const.contact_email:
            # validate localpart and extract domain.
            if contact_value.count('@') != 1:
                raise CerebrumError("Email address (%r) must be on form"
                                    "<localpart>@<domain>" % contact_value)
            localpart, domain = contact_value.split('@')
            domain = domain.lower()    # normalize domain-part
            ea = Email.EmailAddress(self.db)
            ed = Email.EmailDomain(self.db)
            try:
                if not ea.validate_localpart(localpart):
                    raise AttributeError('Invalid local part')
                ed._validate_domain_name(domain)
                #  If checks are okay, reassemble email with normalised domain
                contact_value = localpart + "@" + domain
            except AttributeError as e:
                raise CerebrumError(e)

        # validate phone numbers
        if contact_type in (self.const.contact_phone,
                            self.const.contact_phone_private,
                            self.const.contact_mobile_phone,
                            self.const.contact_private_mobile):
            # allows digits and a prefixed '+'
            if not re.match(r"^\+?\d+$", contact_value):
                raise CerebrumError(
                    "Invalid phone number: %r. "
                    "The number can contain only digits "
                    "with possible '+' for prefix." % contact_value)

        # get existing contact info for this entity and contact type
        contacts = entity.get_contact_info(source=source_system,
                                           type=contact_type)

        existing_prefs = [int(row["contact_pref"]) for row in contacts]

        for row in contacts:
            # if the same value already exists, don't add it
            # case-insensitive check for existing email address
            if contact_value.lower() == row["contact_value"].lower():
                raise CerebrumError("Contact value already exists")
            # if the value is different, add it with a lower (=greater number)
            # preference for the new value
            if contact_pref == row["contact_pref"]:
                contact_pref = max(existing_prefs) + 1
                logger.debug(
                    'Incremented preference, new value = %d' % contact_pref)

        logger.debug('Adding contact info: %r, %r, %r, %r',
                     entity.entity_id,
                     six.text_type(contact_type),
                     contact_value,
                     contact_pref)

        entity.add_contact_info(source_system,
                                type=contact_type,
                                value=contact_value,
                                pref=int(contact_pref),
                                description=None,
                                alias=None)

        return {
            'source_system': six.text_type(source_system),
            'contact_type': six.text_type(contact_type),
            'contact_value': six.text_type(contact_value),
            'entity_type': six.text_type(entity_type),
            'entity_id': int(entity.entity_id),
        }

    #
    # entity contactinfo_remove <entity> <source system> <contact type>
    #
    all_commands['entity_contactinfo_remove'] = Command(
        ("entity", "contactinfo_remove"),
        SimpleString(help_ref='id:target:entity'),
        SourceSystem(help_ref='entity_contact_source_system'),
        SimpleString(help_ref='entity_contact_type'),
        fs=FormatSuggestion([
            ("Removed contact info %s:%s from %s with id=%d",
             ('source_system', 'contact_type', 'entity_type', 'entity_id',)),
            ("Old value: '%s'",
             ('contact_value', )),
            ('Warning: %s', ('warning',)),
        ]),
        perm_filter='can_remove_contact_info')

    def entity_contactinfo_remove(self, operator, entity_target, source_system,
                                  contact_type):
        """Deleting an entity's contact info from a given source system. Useful in
        cases where the entity has old contact information from a source system
        he no longer is exported from, i.e. no affiliations."""

        co = self.const
        contact_type = contact_type.upper()

        # get entity object
        entity = self.util.get_target(entity_target, restrict_to=[])
        entity_type = self.const.EntityType(int(entity.entity_type))

        if not hasattr(entity, 'get_contact_info'):
            raise CerebrumError("No support for contact info in %s entity" %
                                six.text_type(entity_type))

        source_system = self._get_constant(self.const.AuthoritativeSystem,
                                           source_system)
        contact_type = self._get_constant(self.const.ContactInfo, contact_type)

        # check permissions
        self.ba.can_remove_contact_info(operator.get_entity_id(),
                                        entity=entity,
                                        contact_type=contact_type,
                                        source_system=source_system)

        warning = None
        # if the entity is a person...
        if entity_type == co.entity_person:
            # check if person is still affiliated with the given source system
            for a in entity.get_affiliations():
                # allow contact info added manually to be removed
                if (co.AuthoritativeSystem(a['source_system']) is
                        co.system_manual):
                    continue
                if (co.AuthoritativeSystem(a['source_system']) is
                        source_system):
                    warning = (
                        'person has affiliation from source_system %s' %
                        (source_system,))
                    break

        # check if given contact info type exists for this entity
        contact_info = entity.get_contact_info(source=source_system,
                                               type=contact_type)
        if not contact_info:
            raise CerebrumError(
                "Entity does not have contact info type %s in %s" %
                (six.text_type(contact_type),
                 six.text_type(source_system)))
        else:
            contact_info = contact_info.pop(0)

        logger.debug('Removing contact info: %r, %r, %r',
                     entity.entity_id,
                     six.text_type(source_system),
                     six.text_type(contact_type))
        try:
            entity.delete_contact_info(source=source_system,
                                       contact_type=contact_type)
            entity.write_db()
        except Exception:
            raise CerebrumError("Could not remove contact info %s:%s from %r" %
                                (six.text_type(source_system),
                                 six.text_type(contact_type),
                                 entity_target))

        result = {
            'source_system': six.text_type(source_system),
            'contact_type': six.text_type(contact_type),
            'entity_type': six.text_type(entity_type),
            'entity_id': int(entity.entity_id),
        }

        try:
            self.ba.can_get_contact_info(operator.get_entity_id(),
                                         entity=entity,
                                         contact_type=contact_type)
            result['contact_value'] = six.text_type(
                contact_info['contact_value'])
        except PermissionDenied:
            pass

        if warning:
            result['warning'] = warning

        return result

    #
    # entity contactinfo_show <entity>
    #
    all_commands['entity_contactinfo_show'] = Command(
        ("entity", "contactinfo_show"),
        SimpleString(help_ref='id:target:entity'),
        # SimpleString(help_ref='entity_contact_type'),
        fs=FormatSuggestion(
            "%-15s %-15s %-8s %-16s  %s",
            ('source_system', 'contact_type', 'contact_pref',
             format_time('modified'), 'contact_value', ),
            hdr="%-15s %-15s %-8s %-16s  %s" % ('Source', 'Type', 'Weight',
                                                'Modified', 'Value')
        ),
        perm_filter='can_get_contact_info')

    def entity_contactinfo_show(self, operator, entity_target):
        """ Show contact info of an entity. """

        entity = self.util.get_target(entity_target, restrict_to=[])
        entity_type = self.const.EntityType(int(entity.entity_type))

        if not hasattr(entity, 'get_contact_info'):
            raise CerebrumError("No support for contact info in %s entity" %
                                six.text_type(entity_type))

        def get_allowed_types():
            for contact_type in self.const.fetch_constants(
                    self.const.ContactInfo):
                try:
                    self.ba.can_get_contact_info(operator.get_entity_id(),
                                                 entity=entity,
                                                 contact_type=contact_type)
                    yield contact_type
                except PermissionDenied:
                    continue

        contact_types = list(get_allowed_types())
        if not contact_types:
            raise PermissionDenied("Not allowed to see any contact info for %r"
                                   % entity_target)

        contact_info = list()
        for row in entity.get_contact_info(type=contact_types):
            contact_info.append({
                'source_system': six.text_type(
                    self.const.AuthoritativeSystem(row['source_system'])),
                'contact_type': six.text_type(
                    self.const.ContactInfo(row['contact_type'])),
                'contact_pref': six.text_type(row['contact_pref']),
                'contact_value': row['contact_value'],
                'description': row['description'],
                'contact_alias': row['contact_alias'],
                'modified': row['last_modified'],
            })

        if not contact_info:
            raise CerebrumError("No contact info for %r" % entity_target)

        return contact_info