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)]
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)
class BofhdExtension(BofhdCommandBase): all_commands = {} @classmethod def get_help_strings(cls): group_help = { 'password': "******", } command_help = { 'misc': { 'password_info': 'Show if SMS pw service is available for ac', }, } arg_help = { 'account_name': [ 'uname', 'Enter account name', 'Enter the name of the account for this operation' ], } return (group_help, command_help, arg_help) # # misc password_issues # all_commands['misc_password_issues'] = Command( ("misc", "password_issues"), AccountName(help_ref="id:target:account"), fs=FormatSuggestion([ ('\nSMS service is %s for %s!\n', ('sms_work_p', 'accountname')), ('Issues found:\n' ' - %s', ('issue0', )), (' - %s', ('issuen', )), ('Mobile phone numbers and affiliations:\n' ' - %s %s %s %s', ('ssys0', 'status0', 'number0', 'date_str0')), (' - %s %s %s %s', ('ssysn', 'statusn', 'numbern', 'date_strn')), ('Additional info:\n' ' - %s', ('info0', )), (' - %s', ('infon', )), ]), perm_filter='can_set_password') def misc_password_issues(self, operator, accountname): """Determine why a user can't use the SMS service for resetting pw. The cause(s) of failure and/or possibly relevant additional information is returned. There are two kinds of issues: Category I issues raises an error without further testing. Cathegory II issues may require a bit more detective work from Houston. COnsequently, all checks are performed in case more than one issue is present. If no potential problems are found, this is clearly stated. The authoritative check is performed by pofh, and this function duplicates the same checks (and performs some additional ones). """ # Primary intended users are Houston. # They are privileged, but not superusers. ac = self._get_account(accountname, idtype='name') if not self.ba.can_set_password(operator.get_entity_id(), ac): raise PermissionDenied("Access denied") pwi = PassWordIssues(ac, self.db) pwi() return pwi.data
class BofhdApiKeyCommands(BofhdCommandBase): """API subscription commands.""" all_commands = {} authz = BofhdApiKeyAuth @classmethod def get_help_strings(cls): """Get help strings.""" return merge_help_strings( (HELP_GROUP, HELP_CMD, HELP_ARGS), get_help_strings()) # # api subscription_set <identifier> <account> [description] # all_commands['api_subscription_set'] = Command( ('api', 'subscription_set'), SimpleString(help_ref='api_client_identifier'), AccountName(), SimpleString(help_ref='api_client_description', optional=True), fs=FormatSuggestion( "Set subscription='%s' for account %s (%d)", ('identifier', 'account_name', 'account_id') ), perm_filter='can_modify_api_mapping', ) def api_subscription_set(self, operator, identifier, account_id, description=None): """Set api client identifier to user mapping""" # check araguments if not identifier: raise CerebrumError("Invalid identifier") account = self._get_account(account_id) # check permissions self.ba.can_modify_api_mapping(operator.get_entity_id(), account=account) keys = ApiMapping(self.db) try: row = keys.get(identifier) if row['account_id'] != account.entity_id: raise CerebrumError("Identifier already assigned") except Errors.NotFoundError: pass keys.set(identifier, account.entity_id, description) return { 'identifier': identifier, 'account_id': account.entity_id, 'account_name': account.account_name, 'description': description, } # # api subscription_clear <identifier> # all_commands['api_subscription_clear'] = Command( ('api', 'subscription_clear'), SimpleString(help_ref='api_client_identifier'), fs=FormatSuggestion( "Cleared subscription='%s' from account %s (%d)", ('identifier', 'account_name', 'account_id') ), perm_filter='can_modify_api_mapping', ) def api_subscription_clear(self, operator, identifier): """Remove mapping for a given api client identifier""" if not identifier: raise CerebrumError("Invalid identifier") keys = ApiMapping(self.db) try: mapping = keys.get(identifier) except Errors.NotFoundError: raise CerebrumError("Unknown subscription identifier %r" % (identifier,)) # check permissions account = self.Account_class(self.db) account.find(mapping['account_id']) self.ba.can_modify_api_mapping(operator.get_entity_id(), account=account) if not mapping: raise CerebrumError('No identifier=%r for account %s (%d)' % (identifier, account.account_name, account.entity_id)) keys.delete(identifier) return { 'identifier': mapping['identifier'], 'account_id': account.entity_id, 'account_name': account.account_name, 'description': mapping['description'], } # # api subscription_list <account> # all_commands['api_subscription_list'] = Command( ('api', 'subscription_list'), AccountName(), fs=FormatSuggestion( "%-36s %-16s %s", ('identifier', format_time('updated_at'), 'description'), hdr="%-36s %-16s %s" % ('Identifier', 'Last update', 'Description') ), perm_filter='can_list_api_mapping', ) def api_subscription_list(self, operator, account_id): """List api client mappings for a given user.""" account = self._get_account(account_id) self.ba.can_list_api_mapping(operator.get_entity_id(), account=account) keys = ApiMapping(self.db) return [ { 'account_id': row['account_id'], 'identifier': row['identifier'], # TODO: Add support for naive and localized datetime objects in # native_to_xmlrpc 'updated_at': datetime2mx(row['updated_at']), 'description': row['description'], } for row in keys.search(account_id=account.entity_id) ] # # api subscription_info <identifier> # all_commands['api_subscription_info'] = Command( ('api', 'subscription_info'), SimpleString(help_ref='api_client_identifier'), fs=FormatSuggestion( "\n".join(( "Identifier: %s", "Account: %s (%d)", "Last update: %s", "Description: %s", )), ('identifier', 'account_name', 'account_id', format_time('updated_at'), 'description'), ), perm_filter='can_list_api_mapping', ) def api_subscription_info(self, operator, identifier): """List api client mappings for a given user.""" if not self.ba.can_list_api_mapping(operator.get_entity_id(), query_run_any=True): # Abort early if user has no access to list *any* api mappings, # otherwise we may leak info on valid identifiers. raise no_access_error keys = ApiMapping(self.db) try: mapping = keys.get(identifier) except Errors.NotFoundError: raise CerebrumError("Unknown subscription identifier %r" % (identifier,)) account = self.Account_class(self.db) account.find(mapping['account_id']) self.ba.can_list_api_mapping(operator.get_entity_id(), account=account) return { 'account_id': account.entity_id, 'account_name': account.account_name, 'identifier': mapping['identifier'], # TODO: Add support for naive and localized datetime objects in # native_to_xmlrpc 'updated_at': datetime2mx(mapping['updated_at']), 'description': mapping['description'], }
class 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
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)
class BofhdExtension(BofhdCommandBase): """Class with 'user create_unpersonal' method.""" all_commands = {} authz = BofhdUnpersonalAuth @classmethod def get_help_strings(cls): const = Factory.get('Constants')() account_types = const.fetch_constants(const.Account) cmd_args = {} list_sep = '\n - ' for key, value in CMD_ARGS.items(): cmd_args[key] = value[:] if key == 'unpersonal_account_type': cmd_args[key][2] += '\nValid account types:' cmd_args[key][2] += list_sep + list_sep.join( six.text_type(c) for c in account_types) del const return merge_help_strings( ({}, {}, cmd_args), # We want _our_ cmd_args to win! get_help_strings(), ({}, CMD_HELP, {})) # # user create_unpersonal # all_commands['user_create_unpersonal'] = Command( ('user', 'create_unpersonal'), AccountName(), GroupName(), EmailAddress(), SimpleString(help_ref="unpersonal_account_type"), fs=FormatSuggestion("Created account_id=%i", ("account_id",)), perm_filter='can_create_user_unpersonal') def user_create_unpersonal(self, operator, account_name, group_name, contact_address, account_type): """Bofh command: user create_unpersonal""" self.ba.can_create_user_unpersonal(operator.get_entity_id(), group=self._get_group(group_name)) account_type = self._get_constant(self.const.Account, account_type, "account type") account_policy = AccountPolicy(self.db) try: account = account_policy.create_group_account( operator.get_entity_id(), account_name, self._get_group(group_name), contact_address, account_type ) except InvalidAccountCreationArgument as e: raise CerebrumError(e) self._user_password(operator, account) # TBD: Better way of checking if email forwards are in use, by # checking if bofhd command is available? if hasattr(self, '_email_create_forward_target'): localaddr = '{}@{}'.format( account_name, Email.get_primary_default_email_domain()) self._email_create_forward_target(localaddr, contact_address) return {'account_id': int(account.entity_id)}
class BofhdExtension(BofhdCommonMethods): 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
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'
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}
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'
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}