示例#1
0
class BofhdWofhCommands(BofhdCommonMethods):

    all_commands = {}
    hidden_commands = {}
    authz = BofhdAuth

    #
    # group all_account_memberships
    #
    hidden_commands['wofh_all_group_memberships'] = Command(
        ('wofh', 'all_group_memberships'), AccountName())

    def wofh_all_group_memberships(self, operator, account_name):
        """
        Hidden command used by brukerinfo/WOFH.

        Returns all groups associated with an account. If a account is the
        primary we add any person groups as if primary account was a member.
        """
        account = self._get_entity('account', account_name)
        member_id = [account.entity_id]

        if account.owner_type == self.const.entity_person:
            person = Utils.Factory.get('Person')(self.db)
            person.clear()
            person.find(account.owner_id)
            if account.entity_id == person.get_primary_account():
                # Found primary account, add person memberships.
                member_id.append(person.entity_id)

        group_memberships = GroupMemberships(self.db)
        return [{
            'entity_id':
            row['group_id'],
            'group':
            row['name'],
            'description':
            row['description'],
            'expire_date':
            row['expire_date'],
            'group_type':
            text_type(self.const.GroupType(row['group_type']))
        } for row in group_memberships.get_groups(member_id)]
示例#2
0
class BofhdExtension(BofhdCommandBase):
    all_commands = {}

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'user': "******",
        }

        # The texts in command_help are automatically line-wrapped, and should
        # not contain \n
        command_help = {
            'user': {
                'user_info': 'Show information about a user',
            },
        }

        arg_help = {
            'account_name': [
                'uname', 'Enter account name',
                'Enter the name of the account for this operation'
            ],
        }
        return (group_help, command_help, arg_help)

    #
    # user info <id>
    #
    all_commands['user_info'] = Command(
        ("user", "info"),
        AccountName(),
        fs=FormatSuggestion([("Entity id:     %i\n"
                              "Expire:        %s", ("entity_id",
                                                    format_day("expire"))),
                             ("Quarantined:   %s", ("quarantined", ))]))

    def user_info(self, operator, accountname):
        account = self._get_account(accountname, idtype='name')

        ret = {'entity_id': account.entity_id, 'expire': account.expire_date}
        if account.get_entity_quarantine():
            ret['quarantined'] = 'Yes'
        return ret
示例#3
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}
示例#4
0
class BofhdExtension(bofhd_uio_cmds.BofhdExtension):

    all_commands = {}
    hidden_commands = {}
    omit_parent_commands = {
        # UiT does not have the default host_info command - why have this?
        'host_info',

        # UiT does not allow a force option
        'group_delete',

        # UiT implements their own
        'misc_check_password',

        # UiT implements their own
        'person_info',

        # UiT implements their own
        'person_student_info',

        'user_create_sysadm',

        # We include user_restore (for the command definition and prompt_func),
        # but override the actual method in order to add some hooks.
        # 'user_restore',
    }
    parent_commands = True

    authz = bofhd_auth.UitAuth
    external_id_mappings = {}

    def __init__(self, *args, **kwargs):
        super(BofhdExtension, self).__init__(*args, **kwargs)
        self.external_id_mappings['studnr'] = self.const.externalid_studentnr

    @classmethod
    def get_help_strings(cls):
        groups, cmds, args = super(BofhdExtension, cls).get_help_strings()

        # Move help for the 'user history' command to new key
        history = cmds['user'].get('user_history')
        cmds['user']['user_history_filtered'] = history

        return groups, cmds, args

    #
    # group delete <groupname>
    #
    # TODO: UiO includes a force-flag to group_delete
    #
    all_commands['group_delete'] = Command(
        ("group", "delete"),
        GroupName(),
        perm_filter='can_delete_group')

    def group_delete(self, operator, groupname):

        grp = self._get_group(groupname)
        self.ba.can_delete_group(operator.get_entity_id(), grp)
        if grp.group_name == cereconf.BOFHD_SUPERUSER_GROUP:
            raise CerebrumError("Can't delete superuser group")
        # exchange-relatert-jazz
        # it should not be possible to remove distribution groups via
        # bofh, as that would "orphan" e-mail target. if need be such groups
        # should be nuked using a cerebrum-side script.
        if grp.has_extension('DistributionGroup'):
            raise CerebrumError(
                "Cannot delete distribution groups, use 'group"
                " exchange_remove' to deactivate %s" % groupname)
        elif grp.has_extension('PosixGroup'):
            raise CerebrumError(
                "Cannot delete posix groups, use 'group demote_posix %s'"
                " before deleting." % groupname)
        elif grp.get_extensions():
            raise CerebrumError(
                "Cannot delete group %s, is type %r" % (groupname,
                                                        grp.get_extensions()))

        self._remove_auth_target("group", grp.entity_id)
        self._remove_auth_role(grp.entity_id)
        try:
            grp.delete()
        except self.db.DatabaseError as msg:
            if re.search("group_member_exists", exc_to_text(msg)):
                raise CerebrumError(
                    ("Group is member of groups.  "
                     "Use 'group memberships group %s'") % grp.group_name)
            elif re.search("account_info_owner", exc_to_text(msg)):
                raise CerebrumError(
                    ("Group is owner of an account.  "
                     "Use 'entity accounts group %s'") % grp.group_name)
            raise
        return "OK, deleted group '%s'" % groupname

    #
    # group posix_demote <name>
    #
    # TODO: UiO aborts if the group is a default file group for any user.
    #
    all_commands['group_demote_posix'] = Command(
        ("group", "demote_posix"),
        GroupName(),
        perm_filter='can_force_delete_group')

    def group_demote_posix(self, operator, group):
        try:
            grp = self._get_group(group, grtype="PosixGroup")
        except self.db.DatabaseError as msg:
            if "posix_user_gid" in exc_to_text(msg):
                raise CerebrumError(
                    ("Assigned as primary group for posix user(s). "
                     "Use 'group list %s'") % grp.group_name)
            raise

        self.ba.can_force_delete_group(operator.get_entity_id(), grp)
        grp.demote_posix()
        return "OK, demoted '%s'" % group

    #
    # person info
    #
    # UiT includes the last_seen date in affiliation data
    # UiT includes deceased date
    # UiT does not censor contact info or extids
    #
    all_commands['person_info'] = Command(
        ("person", "info"),
        PersonId(help_ref="id:target:person"),
        fs=FormatSuggestion([
            ("Name:          %s\n"
             "Entity-id:     %i\n"
             "Birth:         %s\n"
             "Deceased:      %s\n"
             "Spreads:       %s", ("name", "entity_id", "birth",
                                   "deceased_date", "spreads")),
            ("Affiliations:  %s [from %s]", ("affiliation_1",
                                             "source_system_1")),
            ("               %s [from %s]", ("affiliation", "source_system")),
            ("Names:         %s [from %s]", ("names", "name_src")),
            ("Fnr:           %s [from %s]", ("fnr", "fnr_src")),
            ("Contact:       %s: %s [from %s]", ("contact_type", "contact",
                                                 "contact_src")),
            ("External id:   %s [from %s]", ("extid", "extid_src"))
        ]),
        perm_filter='can_view_person')

    def person_info(self, operator, person_id):
        try:
            person = self.util.get_target(person_id, restrict_to=['Person'])
        except Errors.TooManyRowsError:
            raise CerebrumError("Unexpectedly found more than one person")
        self.ba.can_view_person(operator.get_entity_id(), person)
        try:
            p_name = person.get_name(
                self.const.system_cached,
                getattr(self.const, cereconf.DEFAULT_GECOS_NAME))
            p_name = p_name + ' [from Cached]'
        except Errors.NotFoundError:
            raise CerebrumError("No name is registered for this person")
        data = [{
            'name': p_name,
            'entity_id': person.entity_id,
            'birth': date_to_string(person.birth_date),
            'deceased_date': date_to_string(person.deceased_date),
            'spreads': ", ".join([text_type(self.const.Spread(x['spread']))
                                  for x in person.get_spread()]),
        }]
        affiliations = []
        sources = []
        last_dates = []
        for row in person.get_affiliations():
            ou = self._get_ou(ou_id=row['ou_id'])
            date = row['last_date'].strftime("%Y-%m-%d")
            last_dates.append(date)
            affiliations.append("%s@%s" % (
                text_type(self.const.PersonAffStatus(row['status'])),
                self._format_ou_name(ou)))
            sources.append(
                text_type(
                    self.const.AuthoritativeSystem(row['source_system'])))
        for ss in cereconf.SYSTEM_LOOKUP_ORDER:
            ss = getattr(self.const, ss)
            person_name = ""
            for t in [self.const.name_first, self.const.name_last]:
                try:
                    person_name += person.get_name(ss, t) + ' '
                except Errors.NotFoundError:
                    continue
            if person_name:
                data.append({
                    'names': person_name,
                    'name_src': text_type(self.const.AuthoritativeSystem(ss)),
                })
        if affiliations:
            data[0]['affiliation_1'] = affiliations[0]
            data[0]['source_system_1'] = sources[0]
            data[0]['last_date_1'] = last_dates[0]

        else:
            data[0]['affiliation_1'] = "<none>"
            data[0]['source_system_1'] = "<nowhere>"
            data[0]['last_date_1'] = "<none>"
        for i in range(1, len(affiliations)):
            data.append({'affiliation': affiliations[i],
                         'source_system': sources[i],
                         'last_date': last_dates[i]})

        try:
            self.ba.can_get_person_external_id(operator, person, None, None)
            # Include fnr. Note that this is not displayed by the main
            # bofh-client, but some other clients (Brukerinfo, cweb) rely
            # on this data.
            for row in person.get_external_id(
                    id_type=self.const.externalid_fodselsnr):
                data.append({
                    'fnr': row['external_id'],
                    'fnr_src': text_type(
                        self.const.AuthoritativeSystem(row['source_system'])),
                })
            # Show external ids
            for extid in (
                    'externalid_fodselsnr',
                    'externalid_paga_ansattnr',
                    'externalid_studentnr',
                    'externalid_pass_number',
                    'externalid_social_security_number',
                    'externalid_tax_identification_number',
                    'externalid_value_added_tax_number'):
                extid_const = getattr(self.const, extid, None)
                if extid_const:
                    for row in person.get_external_id(id_type=extid_const):
                        data.append({
                            'extid': text_type(extid_const),
                            'extid_src': text_type(
                                self.const.AuthoritativeSystem(
                                    row['source_system'])),
                        })
        except PermissionDenied:
            pass

        # Show contact info, if permission checks are implemented
        if hasattr(self.ba, 'can_get_contact_info'):
            for row in person.get_contact_info():
                contact_type = self.const.ContactInfo(row['contact_type'])
                if contact_type not in (self.const.contact_phone,
                                        self.const.contact_mobile_phone,
                                        self.const.contact_phone_private,
                                        self.const.contact_private_mobile):
                    continue
                try:
                    if self.ba.can_get_contact_info(
                            operator.get_entity_id(),
                            entity=person,
                            contact_type=contact_type):
                        data.append({
                            'contact': row['contact_value'],
                            'contact_src': text_type(
                                self.const.AuthoritativeSystem(
                                    row['source_system'])),
                            'contact_type': text_type(contact_type),
                        })
                except PermissionDenied:
                    continue
        return data

    #
    # person student_info
    #
    all_commands['person_student_info'] = Command(
        ("person", "student_info"),
        PersonId(),
        fs=FormatSuggestion([
            ("Studieprogrammer: %s, %s, %s, %s, tildelt=%s->%s privatist: %s",
             ("studprogkode", "studieretningkode", "studierettstatkode",
              "studentstatkode", format_day("dato_tildelt"),
              format_day("dato_gyldig_til"), "privatist")),
            ("Eksamensmeldinger: %s (%s), %s",
             ("ekskode", "programmer", format_day("dato"))),
            ("Underv.meld: %s, %s",
             ("undvkode", format_day("dato"))),
            ("Utd. plan: %s, %s, %d, %s",
             ("studieprogramkode", "terminkode_bekreft", "arstall_bekreft",
              format_day("dato_bekreftet"))),
            ("Semesterregistrert: %s - %s, registrert: %s, endret: %s",
             ("regstatus", "regformkode",
              format_day("dato_endring"), format_day("dato_regform_endret"))),
            ("Semesterbetaling: %s - %s, betalt: %s",
             ("betstatus", "betformkode",
              format_day('dato_betaling'))),
            ("Registrert med status_dod: %s",
             ("status_dod",)),
        ]),
        perm_filter='can_get_student_info')

    def person_student_info(self, operator, person_id):
        person_exists = False
        person = None
        try:
            person = self._get_person(*self._map_person_id(person_id))
            person_exists = True
        except CerebrumError as e:
            # Check if person exists in FS, but is not imported yet, e.g.
            # emnestudents. These should only be listed with limited
            # information.
            if person_id and len(person_id) == 11 and person_id.isdigit():
                try:
                    person_id = fodselsnr.personnr_ok(person_id)
                except Exception:
                    raise e
                self.logger.debug('Unknown person %r, asking FS directly',
                                  person_id)
                self.ba.can_get_student_info(operator.get_entity_id(), None)
                fodselsdato, pnum = person_id[:6], person_id[6:]
            else:
                raise e
        else:
            self.ba.can_get_student_info(operator.get_entity_id(), person)
            fnr = person.get_external_id(
                id_type=self.const.externalid_fodselsnr,
                source_system=self.const.system_fs)
            if not fnr:
                raise CerebrumError("No matching fnr from FS")
            fodselsdato, pnum = fodselsnr.del_fnr(fnr[0]['external_id'])
        ret = []
        try:
            db = database.connect(user=cereconf.FS_USER,
                                  service=cereconf.FS_DATABASE_NAME,
                                  DB_driver=cereconf.DB_DRIVER_ORACLE)
        except database.DatabaseError as e:
            self.logger.warn("Can't connect to FS (%s)", text_type(e))
            raise CerebrumError("Can't connect to FS, try later")
        fs = FS(db)
        for row in fs.student.get_undervisningsmelding(fodselsdato, pnum):
            ret.append({
                'undvkode': row['emnekode'],
                'dato': row['dato_endring'],
            })

        har_opptak = set()
        if person_exists:
            for row in fs.student.get_studierett(fodselsdato, pnum):
                har_opptak.add(row['studieprogramkode'])
                ret.append({
                    'studprogkode': row['studieprogramkode'],
                    'studierettstatkode': row['studierettstatkode'],
                    'studentstatkode': row['studentstatkode'],
                    'studieretningkode': row['studieretningkode'],
                    'dato_tildelt': row['dato_studierett_tildelt'],
                    'dato_gyldig_til': row['dato_studierett_gyldig_til'],
                    'privatist': row['status_privatist'],
                })

            for row in fs.student.get_eksamensmeldinger(fodselsdato, pnum):
                programmer = []
                for row2 in fs.info.get_emne_i_studieprogram(row['emnekode']):
                    if row2['studieprogramkode'] in har_opptak:
                        programmer.append(row2['studieprogramkode'])
                ret.append({
                    'ekskode': row['emnekode'],
                    'programmer': ",".join(programmer),
                    'dato': row['dato_opprettet'],
                })

            for row in fs.student.get_utdanningsplan(fodselsdato, pnum):
                ret.append({
                    'studieprogramkode': row['studieprogramkode'],
                    'terminkode_bekreft': row['terminkode_bekreft'],
                    'arstall_bekreft': row['arstall_bekreft'],
                    'dato_bekreftet': row['dato_bekreftet'],
                })

            def _ok_or_not(input):
                """Helper function for proper feedback of status."""
                if not input or input == 'N':
                    return 'Nei'
                if input == 'J':
                    return 'Ja'
                return input

            semregs = tuple(fs.student.get_semreg(fodselsdato, pnum,
                                                  only_valid=False))
            for row in semregs:
                ret.append({
                    'regstatus': _ok_or_not(row['status_reg_ok']),
                    'regformkode': row['regformkode'],
                    'dato_endring': row['dato_endring'],
                    'dato_regform_endret': row['dato_regform_endret'],
                })
                ret.append({
                    'betstatus': _ok_or_not(row['status_bet_ok']),
                    'betformkode': row['betformkode'],
                    'dato_betaling': row['dato_betaling'],
                })
            # The semreg and sembet lines should always be sent, to make it
            # easier for the IT staff to see if a student have paid or not.
            if not semregs:
                ret.append({
                    'regstatus': 'Nei',
                    'regformkode': None,
                    'dato_endring': None,
                    'dato_regform_endret': None,
                })
                ret.append({
                    'betstatus': 'Nei',
                    'betformkode': None,
                    'dato_betaling': None,
                })

        db.close()
        return ret

    #
    # filtered user history
    #
    # Note: profil.uit.no still calls user_history from bofhd_uio_cmds
    #
    all_commands['user_history_filtered'] = Command(
        ("user", "history"),
        AccountName(help_ref='account_name_id'),
        perm_filter='can_show_history')

    def user_history_filtered(self, operator, accountname):
        self.logger.warn("in user history filtered")
        account = self._get_account(accountname)
        self.ba.can_show_history(operator.get_entity_id(), account)
        ret = []
        timedelta = "%s" % (DateTime.mxDateTime.now() -
                            DateTime.DateTimeDelta(7))
        timeperiod = timedelta.split(" ")

        for r in self.db.get_log_events(0,
                                        subject_entity=account.entity_id,
                                        sdate=timeperiod[0]):
            ret.append(self._format_changelog_entry(r))

        ret_val = ""
        for item in ret:
            ret_val += "\n"
            for key, value in item.items():
                ret_val += "%s\t" % str(value)
        return ret_val

    #
    # user restore
    #
    # all_commands['user_restore'] = Command(
    #     ('user', 'restore'),
    #     prompt_func=user_restore_prompt_func,
    #     perm_filter='can_create_user')
    #
    # TODO: Can we just use the UiO implementation here in stead? Difference is
    # that:
    # - UiO also removes membership from expired groups
    # - UiO restores the group and membership if the group is marked with a
    #   personal_group trait.

    def user_restore(self, operator, accountname, aff_ou, home):
        ac = self._get_account(accountname)
        # Check if the account is deleted or reserved
        if not ac.is_deleted() and not ac.is_reserved():
            raise CerebrumError('Please contact brukerreg to restore %r' %
                                accountname)

        # Checking to see if the home path is hardcoded.
        # Raises CerebrumError if the disk does not exist.
        if not home:
            raise CerebrumError('Home must be specified')
        elif home[0] != ':':  # Hardcoded path
            disk_id, home = self._get_disk(home)[1:3]
        else:
            if not self.ba.is_superuser(operator.get_entity_id()):
                raise PermissionDenied('Only superusers may use hardcoded'
                                       ' path')
            disk_id, home = None, home[1:]

        # Check if the operator can alter the user
        if not self.ba.can_create_user(operator.get_entity_id(), ac, disk_id):
            raise PermissionDenied('User restore is limited')

        # We demote posix
        try:
            pu = self._get_account(accountname, actype='PosixUser')
        except CerebrumError:
            pu = Utils.Factory.get('PosixUser')(self.db)
        else:
            pu.delete_posixuser()
            pu = Utils.Factory.get('PosixUser')(self.db)

        # We remove all old group memberships
        grp = self.Group_class(self.db)
        for row in grp.search(member_id=ac.entity_id):
            grp.clear()
            grp.find(row['group_id'])
            grp.remove_member(ac.entity_id)
            grp.write_db()

        # We remove all (the old) affiliations on the account
        for row in ac.get_account_types(filter_expired=False):
            ac.del_account_type(row['ou_id'], row['affiliation'])

        # Automatic selection of affiliation. This could be used if the user
        # should not choose affiliations.
        # # Sort affiliations according to creation date (newest first), and
        # # try to save it for later. If there exists no affiliations, we'll
        # # raise an error, since we'll need an affiliation to copy from the
        # # person to the account.
        # try:
        #     tmp = sorted(pe.get_affiliations(),
        #                  key=lambda i: i['create_date'], reverse=True)[0]
        #     ou, aff = tmp['ou_id'], tmp['affiliation']
        # except IndexError:
        #     raise CerebrumError('Person must have an affiliation')

        # We set the affiliation selected by the operator.
        self._user_create_set_account_type(ac,
                                           ac.owner_id,
                                           aff_ou['ou_id'],
                                           aff_ou['aff'])

        # And promote posix
        old_uid = self._lookup_old_uid(ac.entity_id)
        if old_uid is None:
            uid = pu.get_free_uid()
        else:
            uid = old_uid

        shell = self.const.posix_shell_bash

        # Populate the posix user, and write it to the database
        pu.populate(uid, None, None, shell, parent=ac,
                    creator_id=operator.get_entity_id())
        try:
            pu.write_db()
        except self.db.IntegrityError as e:
            self.logger.debug("IntegrityError (user_restore): %r", e)
            self.db.rollback()
            raise CerebrumError('Please contact brukerreg in order to restore')

        # Unset the expire date
        ac.expire_date = None

        # Add them spreads
        for s in cereconf.BOFHD_NEW_USER_SPREADS:
            if not pu.has_spread(self.const.Spread(s)):
                pu.add_spread(self.const.Spread(s))

        # And remove them quarantines (except those defined in cereconf)
        for q in ac.get_entity_quarantine():
            if (text_type(self.const.Quarantine(q['quarantine_type']))
                    not in cereconf.BOFHD_RESTORE_USER_SAVE_QUARANTINES):
                ac.delete_entity_quarantine(q['quarantine_type'])

        # We set the new homedir
        default_home_spread = self._get_constant(self.const.Spread,
                                                 cereconf.DEFAULT_HOME_SPREAD,
                                                 'spread')

        homedir_id = pu.set_homedir(
            disk_id=disk_id, home=home,
            status=self.const.home_status_not_created)
        pu.set_home(default_home_spread, homedir_id)

        # We'll set a new password and store it for printing
        passwd = ac.make_passwd(ac.account_name)
        ac.set_password(passwd)

        operator.store_state('new_account_passwd',
                             {'account_id': int(ac.entity_id),
                              'password': passwd})

        # We'll need to write to the db, in order to store stuff.
        try:
            ac.write_db()
        except self.db.IntegrityError as e:
            self.logger.debug("IntegrityError (user_restore): %r", e)
            self.db.rollback()
            raise CerebrumError('Please contact brukerreg in order to restore')

        # Return string with some info
        if ac.get_entity_quarantine():
            note = '\nNotice: Account is quarantined!'
        else:
            note = ''

        if old_uid is None:
            tmp = ', new uid=%i' % uid
        else:
            tmp = ', reused old uid=%i' % old_uid

        return ('OK, promoted %s to posix user%s.\n'
                'Password altered. Use misc list_password to print or view '
                'the new password.%s' % (accountname, tmp, note))

    def _format_ou_name(self, ou):
        """
        Override _format_ou_name to support OUs without SKO.
        """
        short_name = ou.get_name_with_language(
            name_variant=self.const.ou_name_short,
            name_language=self.const.language_nb,
            default="")

        # return None if ou does not have stedkode
        if ou.fakultet is not None:
            return "%02i%02i%02i (%s)" % (ou.fakultet, ou.institutt,
                                          ou.avdeling, short_name)
        else:
            return "None"
示例#5
0
class BofhdExtension(BofhdCommandBase):
    all_commands = {}

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'password': "******",
        }

        command_help = {
            'misc': {
                'password_info': 'Show if SMS pw service is available for ac',
            },
        }
        arg_help = {
            'account_name': [
                'uname', 'Enter account name',
                'Enter the name of the account for this operation'
            ],
        }
        return (group_help, command_help, arg_help)

    #
    # misc password_issues
    #
    all_commands['misc_password_issues'] = Command(
        ("misc", "password_issues"),
        AccountName(help_ref="id:target:account"),
        fs=FormatSuggestion([
            ('\nSMS service is %s for %s!\n', ('sms_work_p', 'accountname')),
            ('Issues found:\n'
             ' - %s', ('issue0', )),
            (' - %s', ('issuen', )),
            ('Mobile phone numbers and affiliations:\n'
             ' - %s %s %s %s', ('ssys0', 'status0', 'number0', 'date_str0')),
            (' - %s %s %s %s', ('ssysn', 'statusn', 'numbern', 'date_strn')),
            ('Additional info:\n'
             ' - %s', ('info0', )),
            (' - %s', ('infon', )),
        ]),
        perm_filter='can_set_password')

    def misc_password_issues(self, operator, accountname):
        """Determine why a user can't use the SMS service for resetting pw.

        The cause(s) of failure and/or possibly relevant additional
        information is returned.  There are two kinds of issues:
        Category I issues raises an error without further
        testing. Cathegory II issues may require a bit more detective
        work from Houston. COnsequently, all checks are performed in
        case more than one issue is present.  If no potential problems
        are found, this is clearly stated.

        The authoritative check is performed by pofh, and this function
        duplicates the same checks (and performs some additional ones).
        """

        # Primary intended users are Houston.
        # They are privileged, but not superusers.
        ac = self._get_account(accountname, idtype='name')
        if not self.ba.can_set_password(operator.get_entity_id(), ac):
            raise PermissionDenied("Access denied")
        pwi = PassWordIssues(ac, self.db)
        pwi()
        return pwi.data
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'],
        }
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
            all_args.pop(0)

        # Done
        if len(all_args) == 0:
            return {
                'last_arg': True,
            }
        raise CerebrumError("Too many arguments: %r" % all_args)

    #
    # misc print_passwords [template [printer] [range] [print_user]]
    #
    # TODO: Should the access to this command be restricted?
    #
    all_commands['misc_print_passwords'] = Command(
        ("misc", "print_passwords"),
        prompt_func=misc_print_passwords_prompt_func)

    def misc_print_passwords(self, operator, *args):
        u""" Print password sheets or letters.

        :param BofhdSession operator: The current session.

        :return str: Lisings of the successful print jobs.

        """
        args = list(args[:])
        template = self.__get_template(args.pop(0))
        destination = self._get_printer(operator, template)
        if not destination:
            destination = args.pop(0)
示例#9
0
class BofhdHistoryCmds(BofhdCommandBase):
    """BofhdExtension for history related commands and functionality."""

    all_commands = {}
    authz = BofhdHistoryAuth

    @property
    def util(self):
        try:
            return self.__util
        except AttributeError:
            self.__util = BofhdUtils(self.db)
            return self.__util

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        group_help = {
            'history': "History related commands",
        }

        command_help = {
            'history': {
                'history_show': 'List changes made to an entity',
            }
        }

        argument_help = {
            'limit_number_of_results': [
                'limit', 'Number of changes to list',
                'Upper limit for how many changes to include, counting '
                'backwards from the most recent. Default (when left empty) '
                'is 0, which means no limit'
            ],
            'yes_no_all_changes': [
                'all', 'All involved changes?',
                'List all changes where the entity is involved (yes), or '
                'only the ones where the entity itself is changed (no)'
            ],
        }

        return merge_help_strings(
            get_help_strings(),
            (group_help, command_help, argument_help),
        )

    #
    # history show <entity> [yes|no] [n]
    #
    default_any_entity = 'yes'
    default_num_changes = '0'  # 0 means no limit

    all_commands['history_show'] = Command(
        ('history', 'show'),
        Id(help_ref='id:target:entity'),
        YesNo(help_ref='yes_no_all_changes',
              optional=True,
              default=default_any_entity),
        Integer(help_ref='limit_number_of_results',
                optional=True,
                default=default_num_changes),
        fs=FormatSuggestion('%s [%s]: %s',
                            ('timestamp', 'change_by', 'message')),
        perm_filter='can_show_history',
    )

    def history_show(self,
                     operator,
                     entity,
                     any_entity=default_any_entity,
                     limit_number_of_results=default_num_changes):
        """ show audit log for a given entity. """
        ent = self.util.get_target(entity, restrict_to=[])
        self.ba.can_show_history(operator.get_entity_id(), ent)
        ret = []
        try:
            n = int(limit_number_of_results)
        except ValueError:
            raise CerebrumError('Illegal range limit, must be an integer: '
                                '{}'.format(limit_number_of_results))

        record_db = AuditLogAccessor(self.db)
        rows = list(record_db.search(entities=ent.entity_id))
        if self._get_boolean(any_entity):
            rows.extend(list(record_db.search(targets=ent.entity_id)))
        rows = sorted(rows, key=lambda r: (r.timestamp, r.record_id))

        _process = AuditRecordProcessor()
        for r in rows[-n:]:
            processed_row = _process(r)
            ret.append({
                'timestamp': processed_row.timestamp,
                'change_by': processed_row.change_by,
                'message': processed_row.message
            })
        return ret
示例#10
0
        ret = []
        for row in self._get_guests():
            try:
                ret.append(self._get_guest_info(row['entity_id']))
            except CerebrumError, e:
                print "Error: %s" % e
                continue
        if not ret:
            raise CerebrumError("Found no guest accounts.")
        return ret

    hidden_commands['guest_reset_password'] = Command(
        ('guest', 'reset_password'),
        AccountName(),
        fs=FormatSuggestion([
            ('New password for user %s, notified %s by SMS.', (
                'username',
                'mobile',
            )),
        ]),
        perm_filter='can_reset_guest_password')

    def guest_reset_password(self, operator, username):
        """ Reset the password of a guest account.

        :param BofhdSession operator: The operator
        :param string username: The username of the guest account

        :return dict: A dictionary with keys 'username' and 'mobile'

        """
        account = self._get_account(username)
示例#11
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
示例#12
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))
示例#13
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}
示例#14
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
示例#15
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
示例#16
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
示例#17
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])
示例#18
0
class BofhdExtension(BofhdCommonMethods):
    u""" Guest commands. """

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

    @classmethod
    def get_help_strings(cls):
        """ Help strings for our commands and arguments. """
        group_help = {
            'guest': "Commands for handling guest users",
        }

        command_help = {
            'guest': {
                'guest_create':
                'Create a new guest user',
                'guest_remove':
                'Deactivate a guest user',
                'guest_info':
                'View information about a guest user',
                'guest_list':
                'List out all guest users for a given owner',
                'guest_list_all':
                'List out all guest users',
                'guest_reset_password':
                '******',
            }
        }

        arg_help = {
            'guest_days': [
                'days', 'Enter number of days',
                'Enter the number of days the guest user should be '
                'active'
            ],
            'guest_fname': [
                'given_name', "Enter guest's given name",
                "Enter the guest's first and middle name"
            ],
            'guest_lname': [
                'family_name', "Enter guest's family name",
                "Enter the guest's (last) family name"
            ],
            'guest_responsible': [
                'responsible', "Enter the responsible user",
                "Enter the user that will be set as the "
                "responsible for the guest"
            ],
            'group_name': [
                'group', 'Enter group name',
                "Enter the group the guest should belong to"
            ],
            'mobile_number': [
                'mobile', 'Enter mobile number',
                "Enter the guest's mobile number, where the "
                "username and password will be sent"
            ],
        }
        return (group_help, command_help, arg_help)

    def _get_owner_group(self):
        """ Get the group that should stand as the owner of the guest accounts.

        Note that this is different from the 'responsible' account for a guest,
        which is stored in a trait.

        The owner group will be created if it doesn't exist.

        @rtype:  Group
        @return: The owner Group object that was found/created.

        """
        gr = self.Group_class(self.db)
        try:
            gr.find_by_name(guestconfig.GUEST_OWNER_GROUP)
            return gr
        except Errors.NotFoundError:
            # Group does not exist, must create it
            pass
        self.logger.info('Creating guest owner group %s' %
                         guestconfig.GUEST_OWNER_GROUP)
        ac = self.Account_class(self.db)
        ac.find_by_name(cereconf.INITIAL_ACCOUNTNAME)
        gr.populate(creator_id=ac.entity_id,
                    visibility=self.const.group_visibility_all,
                    name=guestconfig.GUEST_OWNER_GROUP,
                    description="The owner of all the guest accounts")
        gr.write_db()
        return gr

    def _get_guest_group(self, groupname, operator_id):
        """ Get the given guest group. Gets created it if it doesn't exist.

        @type  groupname: string
        @param groupname:
            The name of the group that the guest should be member of

        @type  operator_id: int
        @param operator_id:
            The entity ID of the bofh-operator, which is used as creator of the
            group if it needs to be created.

        @rtype:  Group
        @return: The Group object that was found/created.

        """
        if not groupname in guestconfig.GUEST_TYPES:
            raise CerebrumError('Given group not defined as a guest group')
        try:
            return self._get_group(groupname)
        except CerebrumError:
            # Mostlikely not created yet
            pass
        self.logger.info('Creating guest group %s' % groupname)
        group = self.Group_class(self.db)
        group.populate(creator_id=operator_id,
                       name=groupname,
                       visibility=self.const.group_visibility_all,
                       description="For guest accounts")
        group.write_db()
        return group

    def _get_guests(self, responsible_id=NotSet, include_expired=True):
        """ Get a list of guest accounts that belongs to a given account.

        @type responsible: int
        @param responsible: The responsible's entity_id

        @type include_expired: bool
        @param include_expired:
            If True, all guests will be returned. If False, guests with a
            'guest_old' quarantine will be filtered from the results. Defaults
            to True.

        @rtype: list
        @return:
            A list of db-rows from ent.list_trait. The interesting keys are
            entity_id, target_id and strval.

        """
        ac = self.Account_class(self.db)
        all = ac.list_traits(code=self.const.trait_guest_owner,
                             target_id=responsible_id)
        if include_expired:
            return all
        # Get entity_ids for expired guests, and filter them out
        expired = [
            q['entity_id'] for q in ac.list_entity_quarantines(
                entity_types=self.const.entity_account,
                quarantine_types=self.const.quarantine_guest_old,
                only_active=True)
        ]
        return filter(lambda a: a['entity_id'] not in expired, all)

    def _get_account_name(self, account_id):
        """ Simple lookup of C{Account.entity_id} -> C{Account.account_name}.

        @type  account_id: int
        @param account_id: The entity_id to look up

        @rtype: string
        @return: The account name

        """
        account = self.Account_class(self.db)
        account.find(account_id)
        return account.account_name

    def _get_guest_info(self, entity_id):
        """ Get info about a given guest user.

        @type  entity_id: int
        @param entity_id: The guest account entity_id

        @rtype: dict
        @return: A dictionary with relevant information about a guest user.
                 Keys: 'username': <string>, 'created': <DateTime>,
                       'expires': <DateTime>, 'name': <string>,
                       'responsible': <int>, 'status': <string>,
                       'contact': <string>'

        """
        account = self.Account_class(self.db)
        account.clear()
        account.find(entity_id)
        try:
            guest_name = account.get_trait(
                self.const.trait_guest_name)['strval']
            responsible_id = account.get_trait(
                self.const.trait_guest_owner)['target_id']
        except TypeError:
            self.logger.debug('Not a guest user: %s', account.account_name)
            raise CerebrumError('%s is not a guest user' %
                                account.account_name)
        # Get quarantine date
        try:
            end_date = account.get_entity_quarantine(
                self.const.quarantine_guest_old)[0]['start_date']
        except IndexError:
            self.logger.warn('No quarantine for guest user %s',
                             account.account_name)
            end_date = account.expire_date

        # Get contect info
        mobile = None
        try:
            mobile = account.get_contact_info(
                source=self.const.system_manual,
                type=self.const.contact_mobile_phone)[0]['contact_value']
        except IndexError:
            pass
        # Get account state
        status = 'active'
        if end_date < DateTime.now():
            status = 'expired'

        return {
            'username': account.account_name,
            'created': account.created_at,
            'expires': end_date,
            'name': guest_name,
            'responsible': self._get_account_name(responsible_id),
            'status': status,
            'contact': mobile
        }

    # guest create
    all_commands['guest_create'] = Command(
        ('guest', 'create'),
        Integer(help_ref='guest_days'),
        PersonName(help_ref='guest_fname'),
        PersonName(help_ref='guest_lname'),
        GroupName(default=guestconfig.GUEST_TYPES_DEFAULT),
        Mobile(optional=(not guestconfig.GUEST_REQUIRE_MOBILE)),
        AccountName(help_ref='guest_responsible', optional=True),
        fs=FormatSuggestion([('Created user %s.', ('username', )),
                             (('SMS sent to %s.'), ('sms_to', ))]),
        perm_filter='can_create_personal_guest')

    def guest_create(self,
                     operator,
                     days,
                     fname,
                     lname,
                     groupname,
                     mobile=None,
                     responsible=None):
        """Create and set up a new guest account."""
        self.ba.can_create_personal_guest(operator.get_entity_id())

        # input validation
        fname = fname.strip()
        lname = lname.strip()
        try:
            days = int(days)
        except ValueError:
            raise CerebrumError('The number of days must be an integer')
        if not (0 < days <= guestconfig.GUEST_MAX_DAYS):
            raise CerebrumError('Invalid number of days, must be in the '
                                'range 1-%d' % guestconfig.GUEST_MAX_DAYS)
        if (not fname) or len(fname) < 2:
            raise CerebrumError(
                'First name must be at least 2 characters long')
        if (not lname) or len(lname) < 1:
            raise CerebrumError(
                'Last name must be at least one character long')
        if len(fname) + len(lname) >= 512:
            raise CerebrumError('Full name must not exceed 512 characters')
        if guestconfig.GUEST_REQUIRE_MOBILE and not mobile:
            raise CerebrumError('Mobile phone number required')

        # TODO/TBD: Change to cereconf.SMS_ACCEPT_REGEX?
        if mobile and not (len(mobile) == 8 and mobile.isdigit()):
            raise CerebrumError(
                'Invalid phone number, must be 8 digits, no spaces')

        guest_group = self._get_guest_group(groupname,
                                            operator.get_entity_id())

        if responsible:
            if not self.ba.is_superuser(operator.get_entity_id()):
                raise PermissionDenied('Only superuser can set responsible')
            ac = self._get_account(responsible)
            responsible = ac.entity_id
        else:
            responsible = operator.get_entity_id()

        end_date = DateTime.now() + days

        # Check the maximum number of guest accounts per user
        # TODO: or should we check per person instead?
        if not self.ba.is_superuser(operator.get_entity_id()):
            nr = len(
                tuple(self._get_guests(responsible, include_expired=False)))
            if nr >= guestconfig.GUEST_MAX_PER_PERSON:
                self.logger.debug("More than %d guests, stopped" %
                                  guestconfig.GUEST_MAX_PER_PERSON)
                raise PermissionDenied('Not allowed to have more than '
                                       '%d active guests, you have %d' %
                                       (guestconfig.GUEST_MAX_PER_PERSON, nr))

        # Everything should now be okay, so we create the guest account
        ac = self._create_guest_account(responsible, end_date, fname, lname,
                                        mobile, guest_group)

        # An extra change log is required in the responsible's log
        ac._db.log_change(responsible,
                          ac.const.guest_create,
                          ac.entity_id,
                          change_params={
                              'owner': str(responsible),
                              'mobile': mobile,
                              'name': '%s %s' % (fname, lname)
                          },
                          change_by=operator.get_entity_id())

        # In case a superuser has set a specific account as the responsible,
        # the event should be logged for both operator and responsible:
        if operator.get_entity_id() != responsible:
            ac._db.log_change(operator.get_entity_id(),
                              ac.const.guest_create,
                              ac.entity_id,
                              change_params={
                                  'owner': str(responsible),
                                  'mobile': mobile,
                                  'name': '%s %s' % (fname, lname)
                              },
                              change_by=operator.get_entity_id())

        # Set the password
        password = ac.make_passwd(ac.account_name)
        ac.set_password(password)
        ac.write_db()

        # Store password in session for misc_list_passwords
        operator.store_state("user_passwd", {
            'account_id': int(ac.entity_id),
            'password': password
        })
        ret = {
            'username': ac.account_name,
            'expire': end_date.strftime('%Y-%m-%d'),
        }

        if mobile:
            msg = guestconfig.GUEST_WELCOME_SMS % {
                'username': ac.account_name,
                'expire': end_date.strftime('%Y-%m-%d'),
                'password': password
            }
            if getattr(cereconf, 'SMS_DISABLE', False):
                self.logger.info(
                    "SMS disabled in cereconf, would send to '%s':\n%s\n",
                    mobile, msg)
            else:
                sms = SMSSender(logger=self.logger)
                if not sms(mobile, msg):
                    raise CerebrumError(
                        "Unable to send message to '%s', aborting" % mobile)
                ret['sms_to'] = mobile

        return ret

    def _create_guest_account(self, responsible_id, end_date, fname, lname,
                              mobile, guest_group):
        """ Helper method for creating a guest account.

        Note that this method does not validate any input, that must already
        have been done before calling this method...

        @rtype:  Account
        @return: The created guest account

        """
        owner_group = self._get_owner_group()
        # Get all settings for the given guest type:
        settings = guestconfig.GUEST_TYPES[guest_group.group_name]

        ac = self.Account_class(self.db)
        name = ac.suggest_unames(self.const.account_namespace,
                                 fname,
                                 lname,
                                 maxlen=guestconfig.GUEST_MAX_LENGTH_USERNAME,
                                 prefix=settings['prefix'],
                                 suffix='')[0]
        if settings['prefix'] and not name.startswith(settings['prefix']):
            # TODO/FIXME: Seems suggest_unames ditches the prefix setting if
            # there's not a lot of good usernames left with the given
            # constraints.
            # We could either fix suggest_uname (but that could lead to
            # complications with the imports), or we could try to mangle the
            # name and come up with new suggestions.
            raise Errors.RealityError("No potential usernames available")

        # TODO: make use of ac.create() instead, when it has been defined
        # properly.
        ac.populate(name=name,
                    owner_type=self.const.entity_group,
                    owner_id=owner_group.entity_id,
                    np_type=self.const.account_guest,
                    creator_id=responsible_id,
                    expire_date=None)
        ac.write_db()

        # Tag the account as a guest account:
        ac.populate_trait(code=self.const.trait_guest_owner,
                          target_id=responsible_id)

        # Save the guest's name:
        ac.populate_trait(code=self.const.trait_guest_name,
                          strval='%s %s' % (fname, lname))

        # Set the quarantine:
        ac.add_entity_quarantine(
            qtype=self.const.quarantine_guest_old,
            creator=responsible_id,
            # TBD: or should creator be bootstrap_account?
            description='Guest account auto-expire',
            start=end_date)

        # Add spreads
        for spr in settings.get('spreads', ()):
            try:
                spr = int(self.const.Spread(spr))
            except Errors.NotFoundError:
                self.logger.warn('Unknown guest spread: %s' % spr)
                continue
            ac.add_spread(spr)

        # Add guest account to correct group
        guest_group.add_member(ac.entity_id)

        # Save the phone number
        if mobile:
            ac.add_contact_info(source=self.const.system_manual,
                                type=self.const.contact_mobile_phone,
                                value=mobile)
        ac.write_db()
        return ac

    #
    # guest remove <guest-name>
    #
    all_commands['guest_remove'] = Command(
        ("guest", "remove"),
        AccountName(),
        perm_filter='can_remove_personal_guest')

    def guest_remove(self, operator, username):
        """ Set a new expire-quarantine that starts now.

        The guest account will be blocked from export to any system.

        """
        account = self._get_account(username)
        self.ba.can_remove_personal_guest(operator.get_entity_id(),
                                          guest=account)

        # Deactivate the account (expedite quarantine) and adjust expire_date
        try:
            end_date = account.get_entity_quarantine(
                self.const.quarantine_guest_old)[0]['start_date']
            if end_date < DateTime.now():
                raise CerebrumError("Account '%s' is already deactivated" %
                                    account.account_name)
            account.delete_entity_quarantine(self.const.quarantine_guest_old)
        except IndexError:
            self.logger.warn(
                'Guest %s didn\'t have expire quarantine, '
                'deactivated anyway.', account.account_name)
        account.add_entity_quarantine(qtype=self.const.quarantine_guest_old,
                                      creator=operator.get_entity_id(),
                                      description='New guest account',
                                      start=DateTime.now())
        account.expire_date = DateTime.now()
        account.write_db()
        return 'Ok, %s quarantined, will be removed' % account.account_name

    #
    # guest info <guest-name>
    #
    all_commands['guest_info'] = Command(
        ("guest", "info"),
        AccountName(),
        perm_filter='can_view_personal_guest',
        fs=FormatSuggestion([('Username:       %s\n' + 'Name:           %s\n' +
                              'Responsible:    %s\n' + 'Created on:     %s\n' +
                              'Expires on:     %s\n' + 'Status:         %s\n' +
                              'Contact:        %s',
                              ('username', 'name', 'responsible',
                               format_date('created'), format_date('expires'),
                               'status', 'contact'))]))

    def guest_info(self, operator, username):
        """ Print stored information about a guest account. """
        account = self._get_account(username)
        self.ba.can_view_personal_guest(operator.get_entity_id(),
                                        guest=account)
        return [self._get_guest_info(account.entity_id)]

    #
    # guest list [guest-name]
    #
    all_commands['guest_list'] = Command(
        ("guest", "list"),
        AccountName(optional=True),
        perm_filter='can_create_personal_guest',
        fs=FormatSuggestion([('%-25s %-30s %-10s %-10s',
                              ('username', 'name', format_date('created'),
                               format_date('expires')))],
                            hdr='%-25s %-30s %-10s %-10s' %
                            ('Username', 'Name', 'Created', 'Expires')))

    def guest_list(self, operator, username=None):
        """ Return a list of guest accounts owned by an entity.

        Defaults to listing guests owned by operator, if no username is given.

        """
        self.ba.can_create_personal_guest(operator.get_entity_id())
        if not username:
            target_id = operator.get_entity_id()
        else:
            account = self._get_account(username)
            target_id = account.entity_id

        ret = []
        for row in self._get_guests(target_id):
            ret.append(self._get_guest_info(row['entity_id']))
        if not ret:
            raise CerebrumError("No guest accounts owned by the user")
        return ret

    #
    # guest list_all
    #
    all_commands['guest_list_all'] = Command(
        ("guest", "list_all"),
        fs=FormatSuggestion(
            [('%-25s %-30s %-15s %-10s %-10s',
              ('username', 'name', 'responsible', format_date('created'),
               format_date('expires')))],
            hdr='%-25s %-30s %-15s %-10s %-10s' %
            ('Username', 'Name', 'Responsible', 'Created', 'End date')),
        perm_filter='is_superuser')

    def guest_list_all(self, operator):
        """ Return a list of all personal guest accounts in Cerebrum. """
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied('Only superuser can list all guests')
        ret = []
        for row in self._get_guests():
            try:
                ret.append(self._get_guest_info(row['entity_id']))
            except CerebrumError, e:
                print "Error: %s" % e
                continue
        if not ret:
            raise CerebrumError("Found no guest accounts.")
        return ret
示例#19
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
示例#20
0
class BofhdEmailRTMixin(BofhdEmailMixinBase):
    """ RT related functions. """

    # TODO: RT ise only in use at UiO. This class has not been tested.
    # TODO: We should probably assert that BofhdCommonBase abd BofhdEmailMixin
    #       is in the MRO. Nothing will work otherwise.

    default_email_rt_commands = {}

    #
    # RT settings
    #

    # Pipe function for RT
    _rt_pipe = '|%s --action %s --queue %s --url %s' % (
        '/local/bin/rt-mailgate', '%(action)s', '%(queue)s',
        'https://%(host)s/')

    # This assumes that the only RE meta character in _rt_pipe is the
    # leading pipe.
    _rt_patt = "^\\" + _rt_pipe % {
        'action': '(\S+)',
        'queue': '(\S+)',
        'host': '(\S+)'
    } + "$"

    #
    # Helper functions
    #

    def _resolve_rt_name(self, queuename):
        """Return queue and host of RT queue as tuple."""
        if queuename.count('@') == 0:
            # Use the default host
            return queuename, "rt.uio.no"
        elif queuename.count('@') > 1:
            raise CerebrumError("Invalid RT queue name: %s" % queuename)
        return queuename.split('@')

    def __get_all_related_rt_targets(self, address):
        """ Locate and return all ETs associated with the RT queue.

        Given any address associated with a RT queue, this method returns
        all the ETs associated with that RT queue. E.g.: 'foo@domain' will
        return 'foo@domain' and 'foo-comment@queuehost'

        If address (EA) is not associated with a RT queue, this method
        raises an exception. Otherwise a list of ET entity_ids is returned.

        @type address: basestring
        @param address:
          One of the mail addresses associated with a RT queue.

        @rtype: sequence (of ints)
        @return:
          A sequence with entity_ids of all ETs related to the RT queue that
          address is related to.

        """
        et = Email.EmailTarget(self.db)
        queue, host = self._get_rt_queue_and_host(address)
        targets = set([])
        for action in ("correspond", "comment"):
            alias = self._rt_pipe % {
                'action': action,
                'queue': queue,
                'host': host,
            }
            try:
                et.clear()
                et.find_by_alias(alias)
            except Errors.NotFoundError:
                continue

            targets.add(et.entity_id)

        if not targets:
            raise CerebrumError("RT queue %s on host %s not found" %
                                (queue, host))

        return targets

    def _get_rt_email_target(self, queue, host):
        """ Get EmailTarget for an RT queue. """
        et = Email.EmailTarget(self.db)
        try:
            et.find_by_alias(self._rt_pipe % {
                'action': "correspond",
                'queue': queue,
                'host': host,
            })
        except Errors.NotFoundError:
            raise CerebrumError("Unknown RT queue %s on host %s" %
                                (queue, host))
        return et

    def _get_rt_queue_and_host(self, address):
        """ Get RT queue and host. """
        et, addr = self._get_email_target_and_address(address)

        try:
            m = re.match(self._rt_patt, et.get_alias())
            return m.group(2), m.group(3)
        except AttributeError:
            raise CerebrumError("Could not get queue and host for %s" %
                                address)

    #
    # email rt_create queue[@host] address [force]
    #
    default_email_rt_commands['email_rt_create'] = Command(
        ("email", "rt_create"),
        RTQueue(),
        EmailAddress(),
        YesNo(help_ref="yes_no_force", optional=True),
        perm_filter='can_rt_create')

    def email_rt_create(self, operator, queuename, addr, force="No"):
        """ Create rt queue. """

        queue, host = self._resolve_rt_name(queuename)
        rt_dom = self._get_email_domain_from_str(host)
        op = operator.get_entity_id()
        self.ba.can_rt_create(op, domain=rt_dom)
        try:
            self._get_rt_email_target(queue, host)
        except CerebrumError:
            pass
        else:
            raise CerebrumError("RT queue %s already exists" % queuename)
        addr_lp, addr_domain_name = self._split_email_address(addr)
        addr_dom = self._get_email_domain_from_str(addr_domain_name)
        if addr_domain_name != host:
            self.ba.can_email_address_add(operator.get_entity_id(),
                                          domain=addr_dom)
        replaced_lists = []

        # Unusual characters will raise an exception, a too short name
        # will return False, which we ignore for the queue name.
        self._is_ok_mailing_list_name(queue)

        # The submission address is only allowed to be short if it is
        # equal to the queue name, or the operator is a global
        # postmaster.
        if not (self._is_ok_mailing_list_name(addr_lp)
                or addr == queue + "@" + host or self.ba.is_postmaster(op)):
            raise CerebrumError("Illegal address for submission: %s" % addr)

        # Check if list exists and is replaceable
        try:
            et, ea = self._get_email_target_and_address(addr)
        except CerebrumError:
            pass
        else:
            raise CerebrumError("Address <{}> is in use".format(addr))

        acc = self._get_account("exim")
        et = Email.EmailTarget(self.db)
        ea = Email.EmailAddress(self.db)
        cmd = self._rt_pipe % {
            'action': "correspond",
            'queue': queue,
            'host': host
        }
        et.populate(self.const.email_target_RT,
                    alias=cmd,
                    using_uid=acc.entity_id)
        et.write_db()

        # Add primary address
        ea.populate(addr_lp, addr_dom.entity_id, et.entity_id)
        ea.write_db()
        epat = Email.EmailPrimaryAddressTarget(self.db)
        epat.populate(ea.entity_id, parent=et)
        epat.write_db()
        for alias in replaced_lists:
            if alias == addr:
                continue
            lp, dom = self._split_email_address(alias)
            alias_dom = self._get_email_domain_from_str(dom)
            ea.clear()
            ea.populate(lp, alias_dom.entity_id, et.entity_id)
            ea.write_db()

        # Add RT internal address
        if addr_lp != queue or addr_domain_name != host:
            ea.clear()
            ea.populate(queue, rt_dom.entity_id, et.entity_id)
            ea.write_db()

        # Moving on to the comment address
        et.clear()
        cmd = self._rt_pipe % {
            'queue': queue,
            'action': "comment",
            'host': host
        }
        et.populate(self.const.email_target_RT,
                    alias=cmd,
                    using_uid=acc.entity_id)
        et.write_db()
        ea.clear()
        ea.populate("%s-comment" % queue, rt_dom.entity_id, et.entity_id)
        ea.write_db()
        msg = "RT queue %s on %s added" % (queue, host)
        if replaced_lists:
            msg += ", replacing mailing list(s) %s" % ", ".join(replaced_lists)
        addr = queue + "@" + host
        self._register_spam_settings(addr, self.const.email_target_RT)
        self._register_filter_settings(addr, self.const.email_target_RT)
        return msg

    #
    # email rt_delete queue[@host]
    #
    default_email_rt_commands['email_rt_delete'] = Command(
        ("email", "rt_delete"),
        EmailAddress(),
        fs=FormatSuggestion([("Deleted address: %s", ("address", ))]),
        perm_filter='can_rt_delete')

    def email_rt_delete(self, operator, queuename):
        """ Delete RT list. """
        queue, host = self._resolve_rt_name(queuename)
        rt_dom = self._get_email_domain_from_str(host)
        self.ba.can_rt_delete(operator.get_entity_id(), domain=rt_dom)
        et = Email.EmailTarget(self.db)
        ea = Email.EmailAddress(self.db)
        epat = Email.EmailPrimaryAddressTarget(self.db)
        result = []

        for target_id in self.__get_all_related_rt_targets(queuename):
            try:
                et.clear()
                et.find(target_id)
            except Errors.NotFoundError:
                continue

            epat.clear()
            try:
                epat.find(et.entity_id)
            except Errors.NotFoundError:
                pass
            else:
                epat.delete()
            for r in et.get_addresses():
                addr = '%(local_part)s@%(domain)s' % r
                ea.clear()
                ea.find_by_address(addr)
                ea.delete()
                result.append({'address': addr})
            et.delete()

        return result

    #
    # email rt_add_address queue[@host] address
    #
    default_email_rt_commands['email_rt_add_address'] = Command(
        ('email', 'rt_add_address'),
        RTQueue(),
        EmailAddress(),
        perm_filter='can_rt_address_add')

    def email_rt_add_address(self, operator, queuename, address):
        """ RT add address. """
        queue, host = self._resolve_rt_name(queuename)
        rt_dom = self._get_email_domain_from_str(host)
        self.ba.can_rt_address_add(operator.get_entity_id(), domain=rt_dom)
        et = self._get_rt_email_target(queue, host)
        lp, dom = self._split_email_address(address)
        ed = self._get_email_domain_from_str(dom)
        if host != dom:
            self.ba.can_email_address_add(operator.get_entity_id(), domain=ed)
        ea = Email.EmailAddress(self.db)
        try:
            ea.find_by_local_part_and_domain(lp, ed.entity_id)
            raise CerebrumError("Address already exists (%s)" % address)
        except Errors.NotFoundError:
            pass
        if not (self._is_ok_mailing_list_name(lp)
                or self.ba.is_postmaster(operator.get_entity_id())):
            raise CerebrumError("Illegal queue address: %s" % address)
        ea.clear()
        ea.populate(lp, ed.entity_id, et.entity_id)
        ea.write_db()
        return ("OK, added '%s' as e-mail address for '%s'" %
                (address, queuename))

    #
    # email rt_remove_address queue address
    #
    default_email_rt_commands['email_rt_remove_address'] = Command(
        ('email', 'rt_remove_address'),
        RTQueue(),
        EmailAddress(),
        perm_filter='can_email_address_delete')

    def email_rt_remove_address(self, operator, queuename, address):
        """ RT remove address. """

        queue, host = self._resolve_rt_name(queuename)
        rt_dom = self._get_email_domain_from_str(host)
        self.ba.can_rt_address_remove(operator.get_entity_id(), domain=rt_dom)
        et = self._get_rt_email_target(queue, host)
        return self._remove_email_address(et, address)

    #
    # email rt_primary_address address
    #
    default_email_rt_commands['email_rt_primary_address'] = Command(
        ("email", "rt_primary_address"),
        RTQueue(),
        EmailAddress(),
        fs=FormatSuggestion([("New primary address: '%s'", ("address", ))]),
        perm_filter="can_rt_address_add")

    def email_rt_primary_address(self, operator, queuename, address):
        """ RT set primary address. """

        queue, host = self._resolve_rt_name(queuename)
        self.ba.can_rt_address_add(
            operator.get_entity_id(),
            domain=self._get_email_domain_from_str(host))
        rt = self._get_rt_email_target(queue, host)
        et, ea = self._get_email_target_and_address(address)
        if rt.entity_id != et.entity_id:
            raise CerebrumError(
                "Address <%s> is not associated with RT queue %s" %
                (address, queuename))
        return self._set_email_primary_address(et, ea, address)
示例#21
0
class BofhdUiTExtension(bofhd_core.BofhdCommonMethods):
    """
    Custom UiT commands for bofhd.
    """

    all_commands = {}
    parent_commands = False
    authz = bofhd_auth.UitAuth

    @classmethod
    def get_help_strings(cls):
        cmds = {
            'misc': {
                'misc_list_legacy_user': '******',
            },
            'user': {
                'user_delete_permanent': 'Delete an account permanently',
            },
        }
        args = {
            'yes_no_sure': [
                'certain',
                'Are you absolutely certain you want to do this? This deletes'
                ' the account completely from the database and can not be'
                ' reversed. (y/n)'
            ]
        }
        return merge_help_strings(bofhd_core_help.get_help_strings(),
                                  ({}, cmds, args))

    #
    # UiT special table for reserved usernames. Usernames that is reserved due
    # to being used in legacy systems
    #
    all_commands['misc_list_legacy_user'] = Command(
        ("misc", "legacy_user"),
        PersonId(),
        fs=FormatSuggestion("%-6s %11s %6s %4s ",
                            ('user_name', 'ssn', 'source', 'type'),
                            hdr="%-6s %-11s %6s %4s" %
                            ('UserID', 'Personnr', 'Source', 'Type')))

    def misc_list_legacy_user(self, operator, personid):
        # TODO: This method leaks personal information
        return list_legacy_users(self.db, personid)

    #
    # Special user delete just for UiT that actually deletes an entity from the
    # database
    #
    all_commands['user_delete_permanent'] = Command(
        ("user", "delete_permanent"),
        AccountName(help_ref='account_name_id_uid'),
        YesNo(help_ref='yes_no_sure'),
        perm_filter='is_superuser',
        fs=FormatSuggestion(
            "Account deleted successfully\n" + "Account name:        %s\n" +
            "Owner:               %s\n" + "New primary account: %s",
            (
                'account_name',
                'owner_name',
                'primary_account_name',
            ),
        ))

    def user_delete_permanent(self, operator, account_name, yesno):
        """ Delete a user from the database

        This command deletes every database entry connected to the entity id of
        an account. It is reserved for use by superusers only and you should
        not be using it unless you are absolutely sure about what you are
        doing.

        :param operator: Cerebrum.Account object of operator
        :param basestring account_name: account name of target account
        :param basestring yesno: 'y' to confirm deletion
        :return: Information about the deleted account and its owner
        :rtype: dict
        :raises CerebrumError: If account name is unknown, or the account owner
            is not a person
        """
        if yesno.lower() != 'y':
            return "Did not receive 'y'. User deletion stopped."

        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Currently limited to superusers")

        ac = self._get_account(account_id=account_name, idtype='name')
        try:
            terminate = entity_terminate.delete(self.db, ac)
        except Errors.NotFoundError:
            raise CerebrumError(
                'Account: {}, not owned by a person. Aborting'.format(
                    account_name))
        return terminate
示例#22
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)}
class BofhdExtension(BofhdCommonMethods):
    """ Commands for getting, setting and unsetting consent. """

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

    def __init__(self, *args, **kwargs):
        """
        """
        super(BofhdExtension, self).__init__(*args, **kwargs)
        # POST:
        for attr in ('ConsentType', 'EntityConsent'):
            if not hasattr(self.const, attr):
                raise RuntimeError('consent: Missing consent constant types')

    @classmethod
    def get_help_strings(cls):
        """ Help strings for consent commands. """
        group, cmd, args = get_help_strings()

        group.setdefault('consent', 'Commands for handling consents')

        cmd.setdefault('consent', dict()).update({
            'consent_set':
            cls.consent_set.__doc__,
            'consent_unset':
            cls.consent_unset.__doc__,
            'consent_info':
            cls.consent_info.__doc__,
            'consent_list':
            cls.consent_list.__doc__,
        })

        args.update({
            'consent_type': [
                'type', 'Enter consent type',
                "'consent list' lists defined consents"
            ],
        })

        return (group, cmd, args)

    def check_consent_support(self, entity):
        """ Assert that entity has EntityConsentMixin.

        :param Cerebrum.Entity entity: The entity to check.

        :raise NotImplementedError: If entity lacks consent support.

        """
        entity_type = self.const.EntityType(entity.entity_type)
        if not isinstance(entity, EntityConsentMixin):
            raise NotImplementedError(
                "Entity type '%s' does not support consent." %
                six.text_type(entity_type))

    def _get_consent(self, consent_ident):
        u""" Get consent constant from constant strval or intval.

        :type consent_ident: int, str, Cerebrum.Constant.ConstantCode
        :param consent_ident: Something to lookup a consent constant by

        :return tuple:
            A tuple consisting of a EntityConsent constant and its ConsentType
            constant.

        :raise Cerebrum.Error.NotFoundError: If the constant cannot be found.

        """
        consent = self.const.human2constant(
            consent_ident, const_type=self.const.EntityConsent)
        if not consent:
            raise CerebrumError("No consent %r" % consent_ident)
        consent_type = self.const.ConsentType(consent.consent_type)
        return (consent, consent_type)

    #
    # consent set <ident> <consent_type>
    #
    all_commands['consent_set'] = Command(
        ('consent', 'set'),
        Id(help_ref="id:target:account"),
        ConsentType(),
        fs=FormatSuggestion(
            "OK: Set consent '%s' (%s) for %s '%s' (entity_id=%s)",
            ('consent_name', 'consent_type', 'entity_type', 'entity_name',
             'entity_id')),
        perm_filter='can_set_consent')

    def consent_set(self, operator, entity_ident, consent_ident):
        """ Set a consent for an entity. """
        entity = self.util.get_target(entity_ident, restrict_to=[])
        self.ba.can_set_consent(operator.get_entity_id(), entity)
        self.check_consent_support(entity)
        consent, consent_type = self._get_consent(consent_ident)
        entity_name = self._get_entity_name(entity.entity_id,
                                            entity.entity_type)
        entity.set_consent(consent)
        entity.write_db()
        return {
            'consent_name': six.text_type(consent),
            'consent_type': six.text_type(consent_type),
            'entity_id': entity.entity_id,
            'entity_type':
            six.text_type(self.const.EntityType(entity.entity_type)),
            'entity_name': entity_name,
        }

    #
    # consent unset <ident> <consent_type>
    #
    all_commands['consent_unset'] = Command(
        ('consent', 'unset'),
        Id(help_ref="id:target:account"),
        ConsentType(),
        fs=FormatSuggestion(
            "OK: Removed consent '%s' (%s) for %s '%s' (entity_id=%s)",
            ('consent_name', 'consent_type', 'entity_type', 'entity_name',
             'entity_id')),
        perm_filter='can_unset_consent')

    def consent_unset(self, operator, entity_ident, consent_ident):
        """ Remove a previously set consent. """
        entity = self.util.get_target(entity_ident, restrict_to=[])
        self.ba.can_unset_consent(operator.get_entity_id(), entity)
        self.check_consent_support(entity)
        consent, consent_type = self._get_consent(consent_ident)
        entity_name = self._get_entity_name(entity.entity_id,
                                            entity.entity_type)
        entity.remove_consent(consent)
        entity.write_db()
        return {
            'consent_name': six.text_type(consent),
            'consent_type': six.text_type(consent_type),
            'entity_id': entity.entity_id,
            'entity_type':
            six.text_type(self.const.EntityType(entity.entity_type)),
            'entity_name': entity_name,
        }

    #
    # consent info <ident>
    #
    all_commands['consent_info'] = Command(
        ('consent', 'info'),
        Id(help_ref="id:target:account"),
        fs=FormatSuggestion(
            '%-15s %-8s %-17s %-17s %s',
            ('consent_name', 'consent_type',
             format_datetime('consent_time_set'),
             format_datetime('consent_time_expire'), 'consent_description'),
            hdr='%-15s %-8s %-17s %-17s %s' %
            ('Name', 'Type', 'Set at', 'Expires at', 'Description')),
        perm_filter='can_show_consent_info')

    def consent_info(self, operator, ident):
        u""" View all set consents for a given entity. """
        entity = self.util.get_target(ident, restrict_to=[])
        self.check_consent_support(entity)
        self.ba.can_show_consent_info(operator.get_entity_id(), entity)

        consents = []
        for row in entity.list_consents(entity_id=entity.entity_id,
                                        filter_expired=False):
            consent, consent_type = self._get_consent(int(row['consent_code']))
            consents.append({
                'consent_name': six.text_type(consent),
                'consent_type': six.text_type(consent_type),
                'consent_time_set': row['time_set'],
                'consent_time_expire': row['expiry'],
                'consent_description': row['description'],
            })
        if not consents:
            name = self._get_entity_name(entity.entity_id, entity.entity_type)
            raise CerebrumError(
                "'%s' (entity_type=%s, entity_id=%s) has no consents set" %
                (name, six.text_type(self.const.EntityType(
                    entity.entity_type)), entity.entity_id))
        return consents

    #
    # consent list
    #
    all_commands['consent_list'] = Command(
        ('consent', 'list'),
        fs=FormatSuggestion(
            '%-15s  %-8s  %s',
            ('consent_name', 'consent_type', 'consent_description'),
            hdr='%-16s %-9s %s' % ('Name', 'Type', 'Description')),
        perm_filter='can_list_consents')

    def consent_list(self, operator):
        """ List all consent types. """
        self.ba.can_list_consents(operator.get_entity_id())
        consents = []
        for consent in self.const.fetch_constants(self.const.EntityConsent):
            consent, consent_type = self._get_consent(int(consent))
            consents.append({
                'consent_name': six.text_type(consent),
                'consent_type': six.text_type(consent_type),
                'consent_description': consent.description,
            })
        if not consents:
            raise CerebrumError("No consent types defined yet")
        return consents
示例#24
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),
        }
示例#25
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
示例#26
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)
示例#27
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)
示例#28
0
class HostPolicyBofhdExtension(BofhdCommandBase):
    u"""Class with commands for manipulating host policies. """

    all_commands = {}
    authz = HostPolicyBofhdAuth

    def __init__(self, *args, **kwargs):
        super(HostPolicyBofhdExtension, self).__init__(*args, **kwargs)
        # TODO: don't know where to get the zone setting from
        self.default_zone = self.const.DnsZone(
            getattr(cereconf, 'DNS_DEFAULT_ZONE', 'uio'))

    @classmethod
    def get_help_strings(cls):
        """Help strings are used by jbofh to give users explanations for groups
        of commands, commands and all command arguments (parameters). The
        arg_help's keys are referencing to either Parameters' _help_ref (TODO:
        or its _type in addition?)"""
        return merge_help_strings(
            ({}, {}, HELP_DNS_ARGS),
            (HELP_POLICY_GROUP, HELP_POLICY_CMDS, HELP_POLICY_ARGS))

    def _get_component(self, comp_id, comp_class=PolicyComponent):
        """Helper method for getting a policy, or a given subtype."""
        comp = comp_class(self.db)
        try:
            if type(comp_id) == int or comp_id.isdigit():
                comp.find(comp_id)
            else:
                comp.find_by_name(comp_id)
        except Errors.NotFoundError:
            if comp_class == Atom:
                raise CerebrumError("Couldn't find atom with id=%r" % comp_id)
            elif comp_class == Role:
                raise CerebrumError("Couldn't find role with id=%r" % comp_id)
            raise CerebrumError("Couldn't find policy with id=%r" % comp_id)
        return comp

    def _get_atom(self, atom_id):
        """Helper method for getting an atom."""
        return self._get_component(atom_id, Atom)

    def _get_role(self, role_id):
        """Helper method for getting a role."""
        return self._get_component(role_id, Role)

    def _get_host(self, host_id):
        """Helper method for getting the DnsOwner for the given host ID, which
        can either be an IP address, an A record or a CName alias."""
        finder = Find(self.db, self.default_zone)

        tmp = host_id.split(".")
        if host_id.find(":") == -1 and tmp[-1].isdigit():
            # host_id is an IP
            owner_id = finder.find_target_by_parsing(host_id, IP_NUMBER)
        else:
            owner_id = finder.find_target_by_parsing(host_id, DNS_OWNER)

        # Check if it is a Cname, if so: update the owner_id
        try:
            cname_record = CNameRecord(self.db)
            cname_record.find_by_cname_owner_id(owner_id)
            owner_id = cname_record.target_owner_id
        except Errors.NotFoundError:
            pass

        dns_owner = DnsOwner.DnsOwner(self.db)
        try:
            dns_owner.find(owner_id)
        except Errors.NotFoundError:
            raise CerebrumError('Unknown host: %r' % host_id)
        return dns_owner

    def _check_if_unused(self, comp):
        """Check if component is unused, i.e. not in any relationship or used as
        a policy for hosts. If it is in use, a CerebrumError is raised, telling
        where it is in use. Note that only one of the types of "usage" is
        explained."""
        tmp = tuple(
            row['dns_owner_name']
            for row in comp.search_hostpolicies(policy_id=comp.entity_id))
        if tmp:
            raise CerebrumError("Policy is in use as policy for: %s" %
                                ', '.join(tmp))
        tmp = tuple(row['source_name']
                    for row in comp.search_relations(target_id=comp.entity_id))
        if tmp:
            raise CerebrumError("Policy is used as target for: %s" %
                                ', '.join(tmp))
        tmp = tuple(row['target_name']
                    for row in comp.search_relations(source_id=comp.entity_id))
        if tmp:
            raise CerebrumError("Policy is used as source for: %s" %
                                ', '.join(tmp))

    def _parse_filters(self,
                       input,
                       filters,
                       default_filter=NotSet,
                       default_value=NotSet,
                       separator=',',
                       type_sep=':'):
        """Parse an input string with different filters and return a dict with
        the different filters set, according to the set options. CerebrumErrors
        are raise in case of invalid input, with explanations to what have
        failed.

        The input string must define filters on the form:

            name1:pattern1,name2:pattern2,...

        the filters are separated by L{separator} (default: ','), and each
        filter has a name and a value, separated by L{type_sep} (default: ':').

        The L{filters} is the dict that defines the available filters. Errors
        are raised if the input contains other types of filters. Example of
        filters:

            'name':     str
            'desc':     str
            'spread':   _is_spread_valid
            'expired':  _parse_date

        The filters' values are callbacks to a method that should validate and
        might reformat the input before it's returned. If a callback raises an
        error, a CerebrumError is given back to the user.

        If an input filter does not specify its filter type, the one defined in
        L{default_filter} is used - which should match a key in L{filters}.

        If L{default_value} is set, this value will be put in all defined
        filters that aren't specified in the input.
        """
        if default_filter is not NotSet and default_filter not in filters:
            raise RuntimeError('Default filter not specified in the filters')
        if not input or input == "":
            raise CerebrumError("No filter specified")
        patterns = {}
        for rule in input.split(separator):
            rule = rule.strip()
            if rule.find(":") != -1:
                type, pattern = rule.split(type_sep, 1)
            elif default_filter is not NotSet:
                # the first defined filter is the default one
                type = default_filter
                pattern = rule
            else:
                raise CerebrumError('Filter type not specified for: %r' % rule)
            type, pattern = type.strip(), pattern.strip()
            if type not in filters:
                raise CerebrumError("Unknown filter type: %r" % type)

            if filters[type] is None:
                patterns[type] = pattern
            else:
                # call callback function:
                # Callbacks should only raise CerebrumErrors, which can be
                # raised directly. Everything else is bugs and should be
                # raised.
                patterns[type] = filters[type](pattern)
        # fill in with default values
        if default_value is not NotSet:
            for f in filters:
                if f not in patterns:
                    patterns[f] = default_value
        return patterns

    def _parse_create_date_range(self, date, separator='--'):
        """Parse a string with a date range and return a tuple of length two
        with DateTime objects, or None, if range is missing. The format has the
        form:

            YYYY-MM-DD--YYYY-MM-DD

        where the end date is optional, and would then default to None.

        The main difference between this method and bofhd_uio_cmds' method
        _parse_date_from_to is that if only only one date is given, this is
        considered the start date and not the end date. In addition we differ
        between not set dates and dates that is explicitly set to None.

        Dates that have not been specified are set to NotSet, but dates that
        have explicitly set to nothing returns None. Examples:

            YYYY-MM-DD              returns (<start>, NotSet)
            YYYY-MM-DD--            returns (<start>, None)
            --YYYY-MM-DD            returns (None,    <end>)
            YYYY-MM-DD--YYYY-MM-DD  returns (<start>, <end>)
            '' (empty string)       returns (NotSet, NotSet)
        """
        date_start = date_end = NotSet
        if date:
            tmp = date.split(separator)
            if len(tmp) == 2:
                date_start = date_end = None
                if tmp[0]:  # string could start with the separator
                    date_start = self._parse_date(tmp[0])
                if tmp[1]:  # string could end with separator
                    date_end = self._parse_date(tmp[1])
            elif len(tmp) == 1:
                date_start = self._parse_date(date)
            else:
                raise CerebrumError("Incorrect date specification: %r" % date)
        return (date_start, date_end)

    # TODO: we miss functionality for setting mutex relationships

    #
    # policy atom_create
    #
    all_commands['policy_atom_create'] = Command(
        ('policy', 'atom_create'),
        AtomName(),
        Description(),
        Foundation(),
        FoundationDate(optional=True),
        perm_filter='is_dns_superuser')

    def policy_atom_create(self,
                           operator,
                           name,
                           description,
                           foundation,
                           foundation_date=None):
        """Adds a new atom and its data.

        It can only consist of lowercased, alpha numrice characters and -.
        """
        self.ba.assert_dns_superuser(operator.get_entity_id())
        atom = Atom(self.db)
        # validate data
        tmp = atom.illegal_attr(description)
        if tmp:
            raise CerebrumError('Illegal description: %r' % tmp)
        tmp = atom.illegal_attr(foundation)
        if tmp:
            raise CerebrumError('Illegal foundation: %r' % tmp)
        foundation_date = self._parse_date(foundation_date)

        # check that name isn't already in use
        try:
            self._get_component(name)
        except CerebrumError:
            pass
        else:
            raise CerebrumError('A policy already exists with name: %r' % name)
        atom.populate(name, description, foundation, foundation_date)
        atom.write_db()
        return "New atom %s created" % atom.component_name

    #
    # policy atom_delete
    #
    all_commands['policy_atom_delete'] = Command(
        ('policy', 'atom_delete'), AtomId(), perm_filter='is_dns_superuser')

    def policy_atom_delete(self, operator, atom_id):
        """Delete an atom.

        Try to delete an atom if it hasn't been used in any policy or
        relationship.
        """
        self.ba.assert_dns_superuser(operator.get_entity_id())
        atom = self._get_atom(atom_id)
        self._check_if_unused(atom)  # will raise CerebrumError

        name = atom.component_name
        atom.delete()
        atom.write_db()
        return "Atom %s deleted" % name

    #
    # policy role_create
    #
    all_commands['policy_role_create'] = Command(
        ('policy', 'role_create'),
        RoleName(),
        Description(),
        Foundation(),
        FoundationDate(optional=True),
        perm_filter='is_dns_superuser')

    def policy_role_create(self,
                           operator,
                           name,
                           description,
                           foundation,
                           foundation_date=None):
        """Adds a new role and its data.

        It can only consist of lowercased, alpha numrice characters and -.
        """
        self.ba.assert_dns_superuser(operator.get_entity_id())
        role = Role(self.db)
        # validate data
        tmp = role.illegal_attr(description)
        if tmp:
            raise CerebrumError('Illegal description: %r' % tmp)
        tmp = role.illegal_attr(foundation)
        if tmp:
            raise CerebrumError('Illegal foundation: %r' % tmp)
        foundation_date = self._parse_date(foundation_date)

        # check that name isn't already in use
        try:
            self._get_component(name)
        except CerebrumError:
            pass
        else:
            raise CerebrumError('A policy already exists with name: %r' % name)
        role.populate(name, description, foundation, foundation_date)
        role.write_db()
        return "New role %s created" % role.component_name

    #
    # policy role_delete
    #
    all_commands['policy_role_delete'] = Command(
        ('policy', 'role_delete'), RoleId(), perm_filter='is_dns_superuser')

    def policy_role_delete(self, operator, role_id):
        """Delete a role.

        Try to delete a given role if it's not in any relationship, or is in
        any policy.
        """
        self.ba.assert_dns_superuser(operator.get_entity_id())
        role = self._get_role(role_id)
        # Check if policy is in use anywhere. The method will raise
        # CerebrumErrors if that's the case:
        self._check_if_unused(role)

        name = role.component_name
        role.delete()
        role.write_db()
        return "Role %s deleted" % name

    #
    # policy rename
    #
    all_commands['policy_rename'] = Command(('policy', 'rename'),
                                            PolicyId(),
                                            PolicyName(),
                                            perm_filter='is_dns_superuser')

    def policy_rename(self, operator, policy_id, name):
        """Rename an existing policy, if the name is not already taken."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        policy = self._get_component(policy_id)

        # check if name is already taken
        try:
            self._get_component(name)
        except CerebrumError:
            pass
        else:
            raise CerebrumError('New name %r is in use' % name)
        old_name = policy.component_name
        policy.component_name = name
        policy.write_db()
        return "Policy %s renamed to %s" % (old_name, name)

    #
    # policy set_description
    #
    all_commands['policy_set_description'] = Command(
        ('policy', 'set_description'),
        PolicyId(),
        Description(),
        perm_filter='is_dns_superuser')

    def policy_set_description(self, operator, policy_id, description):
        """Update the description of an existing policy."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        policy = self._get_component(policy_id)
        policy.description = description
        policy.write_db()
        return "Description updated for %s" % policy.component_name

    #
    # policy set_foundation
    #
    all_commands['policy_set_foundation'] = Command(
        ('policy', 'set_foundation'),
        PolicyId(),
        Foundation(),
        FoundationDate(optional=True),
        perm_filter='is_dns_superuser')

    def policy_set_foundation(self,
                              operator,
                              policy_id,
                              foundation,
                              date=None):
        """Update the foundation data of an existing policy."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        policy = self._get_component(policy_id)
        policy.foundation = foundation
        if date:
            policy.foundation_date = self._parse_date(date)
        policy.write_db()
        return "Foundation updated for %s" % policy.component_name

    #
    # policy add_member
    #
    all_commands['policy_add_member'] = Command(
        ('policy', 'add_member'),
        RoleId(help_ref='role_source'),
        PolicyId(help_ref='policy_target'),
        perm_filter='is_dns_superuser')

    def policy_add_member(self, operator, role_id, member_id):
        """Try to add a given policy as a member of a role."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        try:
            role = self._get_role(role_id)
        except CerebrumError as e:
            # check if it is an atom, and give better feedback
            try:
                self._get_atom(role_id)
            except CerebrumError:
                raise e
            raise CerebrumError("Atoms can't have members")
        member = self._get_component(member_id)

        if role.entity_id == member.entity_id:
            raise CerebrumError("Can't add a role to itself")
        # Check if already a member
        for row in role.search_relations(
                source_id=role.entity_id,
                relationship_code=self.const.hostpolicy_contains,
                indirect_relations=True):
            if row['target_id'] == member.entity_id:
                raise CerebrumError("%s already member of %s (through %s)" %
                                    (member.component_name,
                                     role.component_name, row['source_name']))
        try:
            role.add_relationship(self.const.hostpolicy_contains,
                                  member.entity_id)
        except Errors.ProgrammingError as e:
            # The relationship were not accepted, give the user an explanation
            # of why.

            # TODO: need to check for mutex relationships!

            # Check if member is source in the relationship
            for row in role.search_relations(
                    source_id=member.entity_id,
                    relationship_code=self.const.hostpolicy_contains,
                    indirect_relations=True):
                if row['target_id'] == role.entity_id:
                    raise CerebrumError(
                        "%s is already a parent for %s"
                        " (through %s)" %
                        (member.component_name, role.component_name,
                         row['source_name']))

            # if we got here, we weren't able to explain what is wrong
            self.logger.warn("Unhandled bad relationship: %r", e)
            raise CerebrumError('The membership was not allowed due to'
                                ' constraints')
        role.write_db()
        return "Policy %s is now member of role %s" % (member.component_name,
                                                       role.component_name)

    #
    # policy remove_member
    #
    all_commands['policy_remove_member'] = Command(
        ('policy', 'remove_member'),
        RoleId(),
        PolicyId(),
        perm_filter='is_dns_superuser')

    def policy_remove_member(self, operator, role_id, member_id):
        """Try to remove a given member from a given role."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        role = self._get_role(role_id)
        member = self._get_component(member_id)

        # check if relationship do exists:
        rel = role.search_relations(
            source_id=role.entity_id,
            target_id=member.entity_id,
            relationship_code=self.const.hostpolicy_contains)
        if not tuple(rel):
            raise CerebrumError('%s is not a member of %s' %
                                (member.component_name, role.component_name))

        role.remove_relationship(self.const.hostpolicy_contains,
                                 member.entity_id)
        role.write_db()
        return "Policy %s no longer member of %s" % (member.component_name,
                                                     role.component_name)

    #
    # host list_members
    #
    all_commands['policy_list_members'] = Command(
        ('policy', 'list_members'),
        RoleId(),
        fs=FormatSuggestion('%s %s', ('mem_type', 'mem_name'), hdr='Name'),
        perm_filter='is_dns_superuser')

    def policy_list_members(self, operator, role_id):
        """List out all members of a given role."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        role = self._get_role(role_id)

        def _get_members(roleid, increment=0):
            """Get all direct and indirect members of a given role and return
            them as list of strings. The hierarchy is presented by a space
            increment in the strings, e.g. when listing the role "server":

                database-server
                  postgres-server
                  test-server
                web-server
                  production-server
                    vortex-server
                      caching-server
                    apache-server
                  test-server
            """
            co = self.const
            # TODO: there's probably a quicker solution to left padding:
            inc = ' ' * increment
            members = role.search_relations(
                roleid, relationship_code=co.hostpolicy_contains)
            ret = []
            for row in sorted(members, key=lambda r: r['target_name']):
                type = 'A'
                if row['target_entity_type'] == co.entity_hostpolicy_role:
                    type = 'R'
                ret.append({
                    'mem_name': row['target_name'],
                    'mem_type': '%s%s' % (inc, type),
                })
                if row['target_entity_type'] == co.entity_hostpolicy_role:
                    ret.extend(_get_members(row['target_id'], increment + 2))
            return ret

        return _get_members(role.entity_id)

    #
    # host policy_add
    #
    all_commands['host_policy_add'] = Command(('host', 'policy_add'),
                                              HostId(),
                                              PolicyId(),
                                              perm_filter='is_dns_superuser')

    def host_policy_add(self, operator, dns_owner_id, comp_id):
        """Give a host - dns owner - a policy, i.e. a role/atom."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        host = self._get_host(dns_owner_id)
        policy = self._get_component(comp_id)

        # Do not allow atoms directly on hosts
        if policy.entity_type == self.const.entity_hostpolicy_atom:
            raise CerebrumError('Atoms can not be assigned directly to hosts')

        # check if host already has the policy as direct relation
        for row in policy.search_hostpolicies(policy_id=policy.entity_id,
                                              dns_owner_id=host.entity_id):
            raise CerebrumError('Host %s already has policy %s' %
                                (host.name, policy.component_name))

        # Check if host already has the policy indirectly. Not sure if this
        # should be a part of the API, as it's not directly an error, but more
        # of a way of holding the structure somewhat tidy. Note that one could
        # add a role which have sub roles that is already given to the host,
        # without getting an error for that.
        # TODO: this could be swapped with setting indirect_relations to True?
        def check_member_loop(role_id, check_id):
            """Find a given check_id in the members of a role, and then
            raise a CerebrumError with an explanation for this. Works
            recursively."""
            co = self.const
            for row in policy.search_relations(
                    source_id=role_id,
                    relationship_code=co.hostpolicy_contains):
                if row['target_id'] == check_id:
                    raise CerebrumError(
                        '%s is a member of the role %s '
                        '(direct or indirect) - host already '
                        ' has the role' %
                        (row['target_name'], row['source_name']))
                if row['target_entity_type'] == co.entity_hostpolicy_role:
                    check_member_loop(row['target_id'], check_id)

        if policy.entity_type == self.const.entity_hostpolicy_role:
            for row in policy.search_hostpolicies(dns_owner_id=host.entity_id):
                check_member_loop(row['policy_id'], policy.entity_id)

        # TODO: mutex should be checked here

        policy.add_to_host(host.entity_id)
        return "Policy %s added to host %s" % (policy.component_name,
                                               host.name)

    #
    # host policy_remove
    #
    all_commands['host_policy_remove'] = Command(
        ('host', 'policy_remove'),
        HostId(),
        PolicyId(),
        perm_filter='is_dns_superuser')

    def host_policy_remove(self, operator, dns_owner_id, comp_id):
        """Remove a given policy from a given host."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        host = self._get_host(dns_owner_id)
        policy = self._get_component(comp_id)
        # check that the policy is actually given to the host:
        if not tuple(
                policy.search_hostpolicies(policy_id=policy.entity_id,
                                           dns_owner_id=host.entity_id)):
            raise CerebrumError("Host %s doesn't have policy %s" %
                                (host.name, policy.component_name))
        policy.remove_from_host(host.entity_id)
        return "Policy %s removed from host %s" % (policy.component_name,
                                                   host.name)

    #
    # host policy_list
    #
    all_commands['host_policy_list'] = Command(
        ('host', 'policy_list'),
        HostId(),
        fs=FormatSuggestion('%-20s %-40s', ('policy_name', 'desc'),
                            hdr='%-20s %-40s' % ('Policy:', 'Description:')),
        perm_filter='is_dns_superuser')

    def host_policy_list(self, operator, dns_owner_id):
        """List all roles/atoms associated to a given host."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        host = self._get_host(dns_owner_id)
        policy = PolicyComponent(self.db)
        ret = []
        for row in policy.search_hostpolicies(dns_owner_id=host.entity_id):
            policy.clear()
            policy.find(row['policy_id'])
            ret.append({
                'policy_name': row['policy_name'],
                'desc': policy.description,
            })
        return sorted(ret, key=lambda r: r['policy_name'])

    #
    # policy list_hosts
    #
    all_commands['policy_list_hosts'] = Command(
        ('policy', 'list_hosts'),
        PolicyId(),
        fs=FormatSuggestion('%s', ('host_or_policy', )),
        perm_filter='is_dns_superuser')

    def policy_list_hosts(self, operator, component_id):
        """List all hosts that has a given policy (role/atom)."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        comp = self._get_component(component_id)

        def _get_hosts(policyid,
                       increment=0,
                       already_hosts=[],
                       already_policies=[]):
            """Recursive function for getting all hosts at the given policy and
            its parent policies. Returned as a list of strings, where each
            recursion pads its strings with spaces.

            Note that both policies and hosts are returned, as one needs to see
            what subpolicy a host is targeted through.

                jbofh> polic list_hosts server

                  master-server.uio.no.
                  usit-master.uio.no.
                  crond_running
                    usit-worker-server.uio.no.
                  sharedhost
                    login.uio.no.
                    login.ifi.uio.no.
                    selinux-sharedhosts
                      usit-login.uio.no.
                      logon-test.uio.no.
                    usertestservers
                      test-login.uio.no.
                  abc-server
                    abc-test-server
                      abc-test-server-external
                        abcexttests.uio.no.

            One can differ between hosts and policies in that all hosts are
            returned by their FQDN.

            TBD: Now hosts are only returned through the first found relation,
            even though it can be related to a policy in many, many ways. Not
            decided if all relations should be shown for a host.

            The L{already_policies} contains the policies already listed, to
            avoid listing the same policy twice. The L{already_hosts} contains
            hosts already listed, to avoid listing the same host twice.
            """
            # TODO: there's probably a quicker solution to left padding:
            inc = ' ' * increment

            # get this policy's hosts
            ret = []
            for row in comp.search_hostpolicies(policy_id=policyid):
                h_id = row['dns_owner_id']
                if h_id in already_hosts:
                    continue
                already_hosts.append(h_id)
                ret.append({
                    'host_or_policy':
                    '%s%s' % (inc, row['dns_owner_name']),
                })

            # get parent policies if they have hosts related to them
            parent = comp.search_relations(
                target_id=policyid,
                relationship_code=self.const.hostpolicy_contains)
            for row in sorted(parent, key=lambda r: r['source_name']):
                if row['source_id'] in already_policies:
                    continue
                already_policies.append(row['source_id'])
                subs = _get_hosts(row['source_id'], increment + 2,
                                  already_hosts, already_policies)
                if subs:
                    ret.append({
                        'host_or_policy':
                        '%s%s' % (inc, row['source_name']),
                    })
                    ret.extend(subs)
            return sorted(ret)

        return sorted(_get_hosts(comp.entity_id))

    #
    # policy has_member
    #
    all_commands['policy_has_member'] = Command(
        ('policy', 'has_member'),
        PolicyId(),
        fs=FormatSuggestion('%-20s', ('policy_name', ),
                            hdr='%-20s' % ('Policy', )),
        perm_filter='is_dns_superuser')

    def policy_has_member(self, operator, component_id):
        """List all hosts and/or roles that is related to the given
        component."""
        self.ba.assert_dns_superuser(operator.get_entity_id())
        comp = self._get_component(component_id)

        def _get_parents(policyid, increment=0, already_processed=[]):
            """Get all direct and indirect parents of a given policy and return
            them as list of strings. The hierarchy is presented by a space
            increment in the strings, e.g. when listing the policy
            "abc_test_server":

                test_servers
                  server
                    machine
                abc_environment
                  usit_env
                    unix_any

            We could have used search_relations with indirect_relations=True,
            but then we couldn't see the hierarchy.

            The L{already_processed} contains the policies already listed, to
            avoid listing policies twice.
            """
            # TODO: there's probably a quicker solution to left padding:
            inc = ' ' * increment
            parents = comp.search_relations(
                target_id=policyid,
                relationship_code=self.const.hostpolicy_contains)
            ret = []
            for row in sorted(parents, key=lambda r: r['source_name']):
                ret.append({
                    'policy_name': '%s%s' % (inc, row['source_name']),
                })
                if row['source_id'] not in already_processed:
                    ret.extend(
                        _get_parents(row['source_id'], increment + 2,
                                     already_processed))
                already_processed.append(row['source_id'])
            return ret

        return _get_parents(comp.entity_id, 0, [])

    #
    # policy list_atoms
    #
    all_commands['policy_list_atoms'] = Command(
        ('policy', 'list_atoms'),
        Filter(),
        fs=FormatSuggestion('%-20s %-30s', ('name', 'desc'),
                            hdr='%-20s %-30s' % ('Name', 'Description')))

    def policy_list_atoms(self, operator, filter):
        """Return a list of atoms that match the given filters."""
        # This method is available for everyone
        atom = Atom(self.db)
        filters = self._parse_filters(filter, {
            'name': None,
            'date': self._parse_create_date_range,
            'create': self._parse_create_date_range,
            'desc': None,
            'foundation': None
        },
                                      default_filter='name',
                                      default_value=None)
        date_start = date_end = None
        if filters['date']:
            date_start, date_end = filters['date']
            if date_end is NotSet:  # only the specific date should be used
                date_end = date_start
        create_start = create_end = None
        if filters['create']:
            create_start, create_end = filters['create']
            if create_end is NotSet:  # only the specific date should be used
                create_end = create_start
        ret = []
        for row in atom.search(name=filters['name'],
                               description=filters['desc'],
                               create_start=create_start,
                               create_end=create_end,
                               foundation_start=date_start,
                               foundation_end=date_end,
                               foundation=filters['foundation']):
            ret.append({
                'name': row['name'],
                'desc': row['description'],
            })
        return sorted(ret, key=lambda r: r['name'])

    #
    # policy list_roles
    #
    all_commands['policy_list_roles'] = Command(
        ('policy', 'list_roles'),
        Filter(),
        fs=FormatSuggestion('%-20s %-30s', ('name', 'desc'),
                            hdr='%-20s %-30s' % ('Name', 'Description')))

    def policy_list_roles(self, operator, filter):
        """Return a list of roles that match the given filters."""
        # This method is available for everyone
        role = Role(self.db)
        filters = self._parse_filters(filter, {
            'name': str,
            'date': self._parse_create_date_range,
            'create': self._parse_create_date_range,
            'desc': str,
            'foundation': str
        },
                                      default_filter='name',
                                      default_value=None)
        date_start = date_end = None
        if filters['date']:
            date_start, date_end = filters['date']
            if date_end is NotSet:  # only the specific date should be used
                date_end = date_start
        create_start = create_end = None
        if filters['create']:
            create_start, create_end = filters['create']
            if create_end is NotSet:  # only the specific date should be used
                create_end = create_start
        ret = []
        for row in role.search(name=filters['name'],
                               description=filters['desc'],
                               create_start=create_start,
                               create_end=create_end,
                               foundation_start=date_start,
                               foundation_end=date_end,
                               foundation=filters['foundation']):
            ret.append({
                'name': row['name'],
                'desc': row['description'],
            })
        return sorted(ret, key=lambda r: r['name'])

    #
    # policy info
    #
    all_commands['policy_info'] = Command(
        ('policy', 'info'),
        RoleId(),
        fs=FormatSuggestion([
            ('Name:             %-30s\n'
             'Created:          %-30s\n'
             'Description:      %-30s\n'
             'Foundation:       %-30s\n'
             'Foundation date:  %-30s\n'
             'Type:             %-30s',
             ('name', format_day('create_date'), 'desc', 'foundation',
              format_day('foundation_date'), 'type')),
            ('Relation:         %s (%s)', ('target_rel_name',
                                           'target_rel_type'),
             ('Direct relationships where this role is target:')),
            ('Relation:         %s (%s)', ('rel_name', 'rel_type'),
             ('Direct relationships where this role is source:')),
        ]))

    def policy_info(self, operator, policy_id):
        """Return information about a policy component."""
        # This method is available for everyone
        comp = self._get_component(policy_id)
        ret = [{
            'name': comp.component_name,
            'type': text_type(self.const.EntityType(comp.entity_type)),
            'create_date': comp.created_at,
            'desc': comp.description,
            'foundation': comp.foundation,
            'foundation_date': comp.foundation_date,
        }]
        # check what this component is in relationship with
        for row in comp.search_relations(target_id=comp.entity_id):
            ret.append({
                'target_rel_name': row['source_name'],
                'target_rel_type': row['relationship_str'],
            })
        # if this is a role, add direct relationships where this is the source
        if comp.entity_type == self.const.entity_hostpolicy_role:
            for row in comp.search_relations(source_id=comp.entity_id):
                ret.append({
                    'rel_name': row['target_name'],
                    'rel_type': row['relationship_str'],
                })
        return ret
示例#29
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 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