Example #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)]
Example #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
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"
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)
Example #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'],
        }
Example #7
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
Example #8
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)
Example #9
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)}
Example #10
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
Example #11
0
        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

    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'
Example #12
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}
Example #13
0
        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

    hidden_commands['guest_reset_password'] = Command(
        ('guest', 'reset_password'),
        AccountName(help_ref='guest_account_name'),
        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'
Example #14
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}