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(BofhdCommandBase): """Class to expand bofhd with commands for manipulating subnets.""" all_commands = {} parent_commands = False authz = SubnetBofhdAuth def __init__(self, *args, **kwargs): default_zone = getattr(cereconf, 'DNS_DEFAULT_ZONE', kwargs.pop('default_zone', 'uio')) super(BofhdExtension, self).__init__(*args, **kwargs) self.default_zone = self.const.DnsZone(default_zone) @property def _find(self): try: return self.__find_util except AttributeError: self.__find_util = Utils.Find(self.db, self.default_zone) return self.__find_util def _get_subnet_ipv4(self, subnet_identifier): try: s = Subnet(self.db) s.find(subnet_identifier) return s except (ValueError, SubnetError, DNSError): raise CerebrumError("Unable to find subnet %r" % subnet_identifier) def _get_subnet_ipv6(self, subnet_identifier): try: s = IPv6Subnet(self.db) s.find(subnet_identifier) return s except (ValueError, SubnetError, DNSError): raise CerebrumError("Unable to find subnet %r" % subnet_identifier) def _get_subnet(self, subnet_identifier): try: return self._get_subnet_ipv4(subnet_identifier) except CerebrumError: return self._get_subnet_ipv6(subnet_identifier) @classmethod def get_help_strings(cls): _, _, args = get_help_strings() return merge_help_strings( ({}, {}, args), (HELP_SUBNET_GROUP, HELP_SUBNET_CMDS, HELP_SUBNET_ARGS)) # # subnet info <subnet> # all_commands['subnet_info'] = Command( ("subnet", "info"), SubnetIdentifier(), fs=FormatSuggestion([ ("Subnet: %s", ('subnet', )), ("Entity ID: %s", ('entity_id', )), ("Netmask: %s", ('netmask', )), ("Prefix: %s", ("prefix", )), ("Description: '%s'\n" "Name-prefix: '%s'\n" "VLAN: %s\n" "DNS delegated: %s\n" "IP-range: %s", ("desc", "name_prefix", "vlan", "delegated", "ip_range")), ("Reserved host adresses: %s", ("no_of_res_adr", )), ("Reserved addresses: %s", ("res_adr1", )), (" %s", ('res_adr', )), ("Used addresses: %s\n" "Unused addresses: %s (excluding reserved adr.)", ('used', 'unused')), ])) def subnet_info(self, operator, identifier): """Lists the following information about the given subnet: * Subnett * Netmask * Entity ID * Description * Name-prefix * VLAN number * DNS-delegation status * Range of IPs on subnet * Number of reserved addresses * A list of the reserved adresses """ s = self._get_subnet(identifier) is_ipv6 = isinstance(s, IPv6Subnet) ipc = IPv6Calc if is_ipv6 else IPCalc data = { 'subnet': _subnet_to_identifier(s), 'entity_id': str(s.entity_id), 'desc': s.description, 'delegated': "Yes" if s.dns_delegated else "No", 'name_prefix': s.name_prefix, 'no_of_res_adr': str(s.no_of_reserved_adr) } # ipv4 netmask or ipv6 prefix if isinstance(s, Subnet): data['netmask'] = ipc.netmask_to_ip(s.subnet_mask) else: data['prefix'] = '/' + str(s.subnet_mask) if s.vlan_number is not None: data['vlan'] = str(s.vlan_number) else: data['vlan'] = "(None)" data['ip_range'] = "%s - %s" % (ipc.long_to_ip( s.ip_min), ipc.long_to_ip(s.ip_max)) # Calculate number of used and unused IP-addresses on this subnet # ^^^^^^ excluding reserved addresses uip = self._find.count_used_ips(s.subnet_ip) data['used'] = str(uip) data['unused'] = str(s.ip_max - s.ip_min - uip - 1) reserved_adresses = list(sorted(s.reserved_adr)) if reserved_adresses: data["res_adr1"] = "%s (net)" % ipc.long_to_ip( reserved_adresses.pop(0)) else: data["res_adr1"] = "(None)" ret = [ data, ] if reserved_adresses: last_ip = reserved_adresses.pop() for address in reserved_adresses: ret.append({ 'res_adr': ipc.long_to_ip(address), }) ret.append({ 'res_adr': "%s (broadcast)" % ipc.long_to_ip(last_ip), }) return ret # # subnet set_vlan <subnet> <vlan> # all_commands['subnet_set_vlan'] = Command( ("subnet", "set_vlan"), SubnetIdentifier(), Integer(help_ref="subnet_vlan"), fs=FormatSuggestion([ ("OK; VLAN for subnet %s updated from %i to %i", ('subnet_id', 'old_vlan', 'new_vlan')), ]), perm_filter='is_dns_superuser') def subnet_set_vlan(self, operator, identifier, new_vlan): self.ba.assert_dns_superuser(operator.get_entity_id()) try: new_vlan = int(new_vlan) except: raise CerebrumError("VLAN must be an integer; %r isn't" % new_vlan) s = self._get_subnet(identifier) old_vlan = s.vlan_number s.vlan_number = new_vlan s.write_db(perform_checks=False) return { 'subnet_id': _subnet_to_identifier(s), 'old_vlan': old_vlan, 'new_vlan': new_vlan, } # # subnet set_description <subnet> <description> # all_commands['subnet_set_description'] = Command( ("subnet", "set_description"), SubnetIdentifier(), SimpleString(help_ref="subnet_description"), fs=FormatSuggestion([ ("OK; description for subnet %s updated to '%s'", ('subnet_id', 'new_description')), ])) def subnet_set_description(self, operator, identifier, new_description): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) old_description = s.description or '' s.description = new_description s.write_db(perform_checks=False) subnet_id = _subnet_to_identifier(s) return { 'subnet_id': subnet_id, 'old_description': old_description, 'new_description': new_description, } # # subnet set_name_prefix # all_commands['subnet_set_name_prefix'] = Command( ("subnet", "set_name_prefix"), SubnetIdentifier(), SimpleString(help_ref="subnet_name_prefix"), fs=FormatSuggestion([ ("OK; name_prefix for subnet %s updated from '%s' to '%s'", ('subnet_id', 'old_prefix', 'new_prefix')) ]), perm_filter='is_dns_superuser') def subnet_set_name_prefix(self, operator, identifier, new_prefix): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) old_prefix = s.name_prefix s.name_prefix = new_prefix s.write_db(perform_checks=False) subnet_id = self._subnet_to_identifier(s) return { 'subnet_id': subnet_id, 'old_prefix': old_prefix, 'new_prefix': new_prefix, } # # subnet set_dns_delegated # all_commands['subnet_set_dns_delegated'] = Command( ("subnet", "set_dns_delegated"), SubnetIdentifier(), Force(optional=True), fs=FormatSuggestion([ ("Subnet %s set as delegated to external" " DNS server", ('subnet_id', )), ("Note: %s", ('warning', )), ]), perm_filter='is_dns_superuser') def subnet_set_dns_delegated(self, operator, identifier, force=False): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) subnet_id = _subnet_to_identifier(s) if s.dns_delegated: raise CerebrumError("Subnet %s is already set as being delegated" " to external DNS server" % subnet_id) ret = [{ 'subnet_id': subnet_id, }] if s.has_adresses_in_use(): if not force: raise CerebrumError("Subnet '%s' has addresses in use;" " must force to delegate" % subnet_id) ret.append({'warning': "Subnet has address in use!"}) s.dns_delegated = True s.write_db(perform_checks=False) return ret # # subnet unset_dns_delegated # all_commands['subnet_unset_dns_delegated'] = Command( ("subnet", "unset_dns_delegated"), SubnetIdentifier(), fs=FormatSuggestion([ ("Subnet %s no longer set as delegated to external" " DNS server", ('subnet_id', )), ]), perm_filter='is_dns_superuser') def subnet_unset_dns_delegated(self, operator, identifier): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) subnet_id = _subnet_to_identifier(s) if not s.dns_delegated: raise CerebrumError("Subnet %s is already set as not being" " delegated to external DNS server" % subnet_id) s.dns_delegated = False s.write_db(perform_checks=False) return { 'subnet_id': subnet_id, } # # subnet set_reserved # all_commands['subnet_set_reserved'] = Command( ("subnet", "set_reserved"), SubnetIdentifier(), Integer(help_ref="subnet_reserved"), fs=FormatSuggestion([ ("OK; Number of reserved addresses for subnet %s " "updated from %i to %i", ('subnet_id', 'old_reserved', 'new_reserved')), ("FIY: %s", ('warning', )), ]), perm_filter='is_dns_superuser') def subnet_set_reserved(self, operator, identifier, new_res): self.ba.assert_dns_superuser(operator.get_entity_id()) try: new_res = int(new_res) except: raise CerebrumError("The number of reserved addresses must be " "an integer; %r isn't" % new_res) if new_res < 0: raise CerebrumError("Cannot set number of reserved addresses to " + "a negative number such as '%s'" % new_res) s = self._get_subnet(identifier) old_res = s.no_of_reserved_adr s.no_of_reserved_adr = new_res s.calculate_reserved_addresses() res = [{ 'subnet_id': _subnet_to_identifier(s), 'old_reserved': old_res, 'new_reserved': new_res, }] if new_res > old_res: try: s.check_reserved_addresses_in_use() except SubnetError as se: res.append({ 'warning': text_type(se), }) s.write_db(perform_checks=False) return res
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 BofhdExtension(BofhdCommandBase): all_commands = {} @classmethod def get_help_strings(cls): group_help = { 'password': "******", } command_help = { 'misc': { 'password_info': 'Show if SMS pw service is available for ac', }, } arg_help = { 'account_name': [ 'uname', 'Enter account name', 'Enter the name of the account for this operation' ], } return (group_help, command_help, arg_help) # # misc password_issues # all_commands['misc_password_issues'] = Command( ("misc", "password_issues"), AccountName(help_ref="id:target:account"), fs=FormatSuggestion([ ('\nSMS service is %s for %s!\n', ('sms_work_p', 'accountname')), ('Issues found:\n' ' - %s', ('issue0', )), (' - %s', ('issuen', )), ('Mobile phone numbers and affiliations:\n' ' - %s %s %s %s', ('ssys0', 'status0', 'number0', 'date_str0')), (' - %s %s %s %s', ('ssysn', 'statusn', 'numbern', 'date_strn')), ('Additional info:\n' ' - %s', ('info0', )), (' - %s', ('infon', )), ]), perm_filter='can_set_password') def misc_password_issues(self, operator, accountname): """Determine why a user can't use the SMS service for resetting pw. The cause(s) of failure and/or possibly relevant additional information is returned. There are two kinds of issues: Category I issues raises an error without further testing. Cathegory II issues may require a bit more detective work from Houston. COnsequently, all checks are performed in case more than one issue is present. If no potential problems are found, this is clearly stated. The authoritative check is performed by pofh, and this function duplicates the same checks (and performs some additional ones). """ # Primary intended users are Houston. # They are privileged, but not superusers. ac = self._get_account(accountname, idtype='name') if not self.ba.can_set_password(operator.get_entity_id(), ac): raise PermissionDenied("Access denied") pwi = PassWordIssues(ac, self.db) pwi() return pwi.data
class BofhdApiKeyCommands(BofhdCommandBase): """API subscription commands.""" all_commands = {} authz = BofhdApiKeyAuth @classmethod def get_help_strings(cls): """Get help strings.""" return merge_help_strings( (HELP_GROUP, HELP_CMD, HELP_ARGS), get_help_strings()) # # api subscription_set <identifier> <account> [description] # all_commands['api_subscription_set'] = Command( ('api', 'subscription_set'), SimpleString(help_ref='api_client_identifier'), AccountName(), SimpleString(help_ref='api_client_description', optional=True), fs=FormatSuggestion( "Set subscription='%s' for account %s (%d)", ('identifier', 'account_name', 'account_id') ), perm_filter='can_modify_api_mapping', ) def api_subscription_set(self, operator, identifier, account_id, description=None): """Set api client identifier to user mapping""" # check araguments if not identifier: raise CerebrumError("Invalid identifier") account = self._get_account(account_id) # check permissions self.ba.can_modify_api_mapping(operator.get_entity_id(), account=account) keys = ApiMapping(self.db) try: row = keys.get(identifier) if row['account_id'] != account.entity_id: raise CerebrumError("Identifier already assigned") except Errors.NotFoundError: pass keys.set(identifier, account.entity_id, description) return { 'identifier': identifier, 'account_id': account.entity_id, 'account_name': account.account_name, 'description': description, } # # api subscription_clear <identifier> # all_commands['api_subscription_clear'] = Command( ('api', 'subscription_clear'), SimpleString(help_ref='api_client_identifier'), fs=FormatSuggestion( "Cleared subscription='%s' from account %s (%d)", ('identifier', 'account_name', 'account_id') ), perm_filter='can_modify_api_mapping', ) def api_subscription_clear(self, operator, identifier): """Remove mapping for a given api client identifier""" if not identifier: raise CerebrumError("Invalid identifier") keys = ApiMapping(self.db) try: mapping = keys.get(identifier) except Errors.NotFoundError: raise CerebrumError("Unknown subscription identifier %r" % (identifier,)) # check permissions account = self.Account_class(self.db) account.find(mapping['account_id']) self.ba.can_modify_api_mapping(operator.get_entity_id(), account=account) if not mapping: raise CerebrumError('No identifier=%r for account %s (%d)' % (identifier, account.account_name, account.entity_id)) keys.delete(identifier) return { 'identifier': mapping['identifier'], 'account_id': account.entity_id, 'account_name': account.account_name, 'description': mapping['description'], } # # api subscription_list <account> # all_commands['api_subscription_list'] = Command( ('api', 'subscription_list'), AccountName(), fs=FormatSuggestion( "%-36s %-16s %s", ('identifier', format_time('updated_at'), 'description'), hdr="%-36s %-16s %s" % ('Identifier', 'Last update', 'Description') ), perm_filter='can_list_api_mapping', ) def api_subscription_list(self, operator, account_id): """List api client mappings for a given user.""" account = self._get_account(account_id) self.ba.can_list_api_mapping(operator.get_entity_id(), account=account) keys = ApiMapping(self.db) return [ { 'account_id': row['account_id'], 'identifier': row['identifier'], # TODO: Add support for naive and localized datetime objects in # native_to_xmlrpc 'updated_at': datetime2mx(row['updated_at']), 'description': row['description'], } for row in keys.search(account_id=account.entity_id) ] # # api subscription_info <identifier> # all_commands['api_subscription_info'] = Command( ('api', 'subscription_info'), SimpleString(help_ref='api_client_identifier'), fs=FormatSuggestion( "\n".join(( "Identifier: %s", "Account: %s (%d)", "Last update: %s", "Description: %s", )), ('identifier', 'account_name', 'account_id', format_time('updated_at'), 'description'), ), perm_filter='can_list_api_mapping', ) def api_subscription_info(self, operator, identifier): """List api client mappings for a given user.""" if not self.ba.can_list_api_mapping(operator.get_entity_id(), query_run_any=True): # Abort early if user has no access to list *any* api mappings, # otherwise we may leak info on valid identifiers. raise no_access_error keys = ApiMapping(self.db) try: mapping = keys.get(identifier) except Errors.NotFoundError: raise CerebrumError("Unknown subscription identifier %r" % (identifier,)) account = self.Account_class(self.db) account.find(mapping['account_id']) self.ba.can_list_api_mapping(operator.get_entity_id(), account=account) return { 'account_id': account.entity_id, 'account_name': account.account_name, 'identifier': mapping['identifier'], # TODO: Add support for naive and localized datetime objects in # native_to_xmlrpc 'updated_at': datetime2mx(mapping['updated_at']), 'description': mapping['description'], }
class BofhdExtension(BofhdCommandBase): all_commands = {} hidden_commands = {} authz = RequestsAuth # # misc change_request <request-id> <datetime> # all_commands['misc_change_request'] = Command(("misc", "change_request"), Id(help_ref="id:request_id"), DateTimeString()) def misc_change_request(self, operator, request_id, datetime): if not request_id: raise CerebrumError('Request id required') if not datetime: raise CerebrumError('Date required') datetime = self._parse_date(datetime) br = BofhdRequests(self.db, self.const) old_req = br.get_requests(request_id=request_id) if not old_req: raise CerebrumError("There is no request with id=%r" % request_id) else: # If there is anything, it's at most one old_req = old_req[0] # If you are allowed to cancel a request, you can change it :) self.ba.can_cancel_request(operator.get_entity_id(), request_id) br.delete_request(request_id=request_id) br.add_request(operator.get_entity_id(), datetime, old_req['operation'], old_req['entity_id'], old_req['destination_id'], old_req['state_data']) return "OK, altered request %s" % request_id # # misc list_bofhd_request_types # all_commands['misc_list_bofhd_request_types'] = Command( ("misc", "list_bofhd_request_types"), fs=FormatSuggestion("%-20s %s", ("code_str", "description"), hdr="%-20s %s" % ("Code", "Description"))) def misc_list_bofhd_request_types(self, operator): br = BofhdRequests(self.db, self.const) result = [] for row in br.get_operations(): br_code = self.const.BofhdRequestOp(row['code_str']) result.append({ 'code_str': six.text_type(br_code).lstrip('br_'), 'description': br_code.description, }) return result # # misc cancel_request # all_commands['misc_cancel_request'] = Command( ("misc", "cancel_request"), SimpleString(help_ref='id:request_id')) def misc_cancel_request(self, operator, req): if req.isdigit(): req_id = int(req) else: raise CerebrumError("Request-ID must be a number") br = BofhdRequests(self.db, self.const) if not br.get_requests(request_id=req_id): raise CerebrumError("Request ID %d not found" % req_id) self.ba.can_cancel_request(operator.get_entity_id(), req_id) br.delete_request(request_id=req_id) return "OK, %d canceled" % req_id # # misc list_requests # all_commands['misc_list_requests'] = Command( ("misc", "list_requests"), SimpleString(help_ref='string_bofh_request_search_by', default='requestee'), SimpleString(help_ref='string_bofh_request_target', default='<me>'), fs=FormatSuggestion("%-7i %-10s %-16s %-16s %-10s %-20s %s", ("id", "requestee", format_time("when"), "op", "entity", "destination", "args"), hdr="%-7s %-10s %-16s %-16s %-10s %-20s %s" % ("Id", "Requestee", "When", "Op", "Entity", "Destination", "Arguments"))) def misc_list_requests(self, operator, search_by, destination): br = BofhdRequests(self.db, self.const) ret = [] if destination == '<me>': destination = self._get_account(operator.get_entity_id(), idtype='id') destination = destination.account_name if search_by == 'requestee': account = self._get_account(destination) rows = br.get_requests(operator_id=account.entity_id, given=True) elif search_by == 'operation': try: destination = int( self.const.BofhdRequestOp('br_' + destination)) except Errors.NotFoundError: raise CerebrumError("Unknown request operation %s" % destination) rows = br.get_requests(operation=destination) elif search_by == 'disk': disk_id = self._get_disk(destination)[1] rows = br.get_requests(destination_id=disk_id) elif search_by == 'account': account = self._get_account(destination) rows = br.get_requests(entity_id=account.entity_id) else: raise CerebrumError("Unknown search_by criteria") for r in rows: op = self.const.BofhdRequestOp(r['operation']) dest = None ent_name = None if op in (self.const.bofh_move_user, self.const.bofh_move_request, self.const.bofh_move_user_now): disk = self._get_disk(r['destination_id'])[0] dest = disk.path elif op in (self.const.bofh_move_give, ): dest = self._get_entity_name(r['destination_id'], self.const.entity_group) elif op in (self.const.bofh_email_create, self.const.bofh_email_delete): dest = self._get_entity_name(r['destination_id'], self.const.entity_host) elif op in (self.const.bofh_sympa_create, self.const.bofh_sympa_remove): ea = Email.EmailAddress(self.db) if r['destination_id'] is not None: ea.find(r['destination_id']) dest = ea.get_address() ea.clear() try: ea.find(r['entity_id']) except Errors.NotFoundError: ent_name = "<not found>" else: ent_name = ea.get_address() if ent_name is None: ent_name = self._get_entity_name(r['entity_id'], self.const.entity_account) if r['requestee_id'] is None: requestee = '' else: requestee = self._get_entity_name(r['requestee_id'], self.const.entity_account) ret.append({ 'when': r['run_at'], 'requestee': requestee, 'op': six.text_type(op), 'entity': ent_name, 'destination': dest, 'args': r['state_data'], 'id': r['request_id'] }) ret.sort(lambda a, b: cmp(a['id'], b['id'])) return ret
class BofhdOUDiskMappingCommands(BofhdCommandBase): """OU Disk Mapping commands.""" all_commands = {} authz = BofhdOUDiskMappingAuth @classmethod def get_help_strings(cls): """Get help strings.""" return merge_help_strings((HELP_GROUP, HELP_CMD, HELP_ARGS), get_help_strings()) def __find_ou(self, ou): # Try to find the OU the user wants to edit ou_class = self.OU_class(self.db) if ou.startswith("id:"): ou = ou[3:] # Assume we got the entity id of the ou try: ou_class.find(ou) except Errors.NotFoundError: raise CerebrumError("Unknown OU id {}".format( six.text_type(ou))) elif len(ou) == 6: # Assume we got a stedkode fakultet = ou[:2] institutt = ou[2:4] avdeling = ou[4:] institusjon = cereconf.DEFAULT_INSTITUSJONSNR try: ou_class.find_stedkode(fakultet, institutt, avdeling, institusjon) except Errors.NotFoundError: raise CerebrumError("Unknown OU with stedkode {}".format( six.text_type(ou))) else: raise CerebrumError( "Unable to parse OU id or stedkode {}".format(ou)) return ou_class # # ou homedir_set <path> <ou> <aff> <status> # all_commands["ou_homedir_add"] = Command( ("ou", "homedir_add"), SimpleString(help_ref="path"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "Set homedir='%s' for affiliation %s at OU %i %s", ("path", "aff", "ou", "stedkode"), ), perm_filter="can_add_ou_path", ) def ou_homedir_add(self, operator, disk, ou, aff=None): """Set default home dir for aff at OU :param operator: :param str ou: :param str or None aff: :param str disk: :rtype: dict :return: client output """ ou_class = self.__find_ou(ou) # Check if allowed to set default home dir for this Aff at this OU if not self.ba.can_add_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit aff_str = "*" status = None if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff_str = six.text_type(status) else: aff_str = six.text_type(aff) # Try to find the disk the users want to set disk_class = Factory.get("Disk")(self.db) if disk.startswith("id:") or disk.isdigit(): # Assume this is an entity id try: disk_class.find(disk) except Errors.NotFoundError: raise CerebrumError("Unknown disk with id {}".format( six.text_type(disk))) else: # Assume the user wrote a path try: disk_class.find_by_path(disk) except Errors.NotFoundError: raise CerebrumError("Unknown disk with path {}".format( six.text_type(disk))) # Set the path and return some information to the user ous = OUDiskMapping(self.db) ous.add(ou_class.entity_id, aff, status, disk_class.entity_id) return { "ou": ou_class.entity_id, "aff": aff_str, "path": six.text_type(disk_class.path), "stedkode": "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr( ou_class, 'get_stedkode') else "" } # # ou homedir_clear <ou> <aff> <status> # all_commands["ou_homedir_remove"] = Command( ("ou", "homedir_remove"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "Removed homedir for affiliation %s at OU %i %s", ("aff", "ou", "stedkode"), ), perm_filter="can_remove_ou_path", ) def ou_homedir_remove(self, operator, ou, aff=None): """Remove default home dir for aff at OU If you want to remove the homedir for just the OU and any aff and status, use: > ou homedir_remove ou For OU with affiliation and wildcard status use: > ou homedir_remove ou aff For OU with affiliation and status use: > ou homedir_remove ou aff/status :param operator: :param str ou: :param str or None aff: :rtype: dict :return: client output """ # Try to find the OU the user wants to edit ou_class = self.__find_ou(ou) # Check if allowed to clear home dir for this OU if not self.ba.can_remove_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit aff_str = "*" status = None if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff_str = six.text_type(status) else: aff_str = six.text_type(aff) # Clear the path and return some information to the user ous = OUDiskMapping(self.db) ous.delete(ou_class.entity_id, aff, status) return { "ou": ou_class.entity_id, "aff": aff_str, "stedkode": "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr( ou_class, 'get_stedkode') else "" } # # ou homedir_get <ou> <aff> <status> # all_commands["ou_homedir_list"] = Command( ("ou", "homedir_list"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "%8s %12i %26s %s", ("stedkode", "ou", "aff", "disk"), hdr="%8s %12s %26s %s" % ("Stedkode", "OU", "Affiliation", "Disk"), ), perm_filter="can_list_ou_path", ) def ou_homedir_list(self, operator, ou, aff=None): """Get default home dir for aff at OU :param operator: :param str ou: :param str or None aff: :rtype: dict :return: client output """ if aff is None: aff = NotSet # Try to find the OU the user wants to edit ou_class = self.__find_ou(ou) # Check if allowed to clear home dir for this OU if not self.ba.can_list_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff = status # Get the path and return some information to the user ous = OUDiskMapping(self.db) disk_class = Factory.get("Disk")(self.db) ret = [] for row in ous.search(ou_class.entity_id, aff, any_status=True): if row["status_code"] is not None: aff_str = str(self.const.PersonAffStatus(row["status_code"])) elif row["aff_code"] is not None: aff_str = str(self.const.PersonAffiliation(row["aff_code"])) else: aff_str = None ou_class.clear() ou_class.find(row["ou_id"]) # Get the stedkode of the OU if the stedkode module is present stedkode = (ou_class.get_stedkode() if hasattr( ou_class, "get_stedkode") else "") disk_class.clear() disk_class.find(row["disk_id"]) ret.append({ "stedkode": stedkode, "ou": row["ou_id"], "aff": aff_str, "disk": disk_class.path, }) return ret
class BofhdJobRunnerCommands(BofhdCommandBase): """Bofh commands for job_runner.""" all_commands = {} authz = BofhdJobRunnerAuth @classmethod def get_help_strings(cls): """Get help strings.""" return CMD_GROUP, CMD_HELP, CMD_ARGS def _run_job_runner_command(self, command, args=None): """Run a job_runner command via the job_runner socket.""" with closing(socket.socket(socket.AF_UNIX)) as sock: sock.connect(cereconf.JOB_RUNNER_SOCKET) sock.settimeout(0.2) try: return SocketProtocol.call(SocketConnection(sock), command, args) except socket.timeout: raise CerebrumError('Error talking to job_runner. Socket ' 'timeout') # # job_runner status # all_commands['job_runner_status'] = Command( ("job_runner", "status"), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_show_job_runner_status') def job_runner_status(self, operator): """Show job runner status.""" # Access control self.ba.can_show_job_runner_status(operator.get_entity_id()) return self._run_job_runner_command('STATUS') # job_runner info # all_commands['job_runner_info'] = Command( ("job_runner", "info"), SimpleString(help_ref='job_runner_name', repeat=False), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_show_job_runner_job') def job_runner_info(self, operator, job_name): """Show job runner status.""" # Access control self.ba.can_show_job_runner_job(operator.get_entity_id()) return self._run_job_runner_command('SHOWJOB', [ job_name, ]) # # job_runner run # all_commands['job_runner_run'] = Command( ("job_runner", "run"), SimpleString(help_ref='job_runner_name', repeat=False), YesNo(help_ref='yes_no_with_deps', optional=True, default="No"), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_run_job_runner_job') def job_runner_run(self, operator, job_name, with_deps=False): """Run a job runner job.""" # Access control self.ba.can_run_job_runner_job(operator.get_entity_id()) with_deps = self._get_boolean(with_deps) return self._run_job_runner_command('RUNJOB', [job_name, with_deps])
class BofhdExtension(BofhdCommonMethods): u""" Commands for managing Feide services and multifactor authentication. """ hidden_commands = {} # Not accessible through bofh all_commands = {} parent_commands = False authz = BofhdAuth def _find_service(self, service_name): fse = FeideService(self.db) try: fse.find_by_name(service_name) except Errors.NotFoundError: raise CerebrumError('No such Feide service') return fse @classmethod def get_help_strings(cls): u""" Help strings for Feide commands. """ group_help = { 'feide': 'Commands for Feide multifactor authentication', } command_help = { 'feide': { 'feide_service_add': cls.feide_service_add.__doc__, 'feide_service_remove': cls.feide_service_remove.__doc__, 'feide_service_list': cls.feide_service_list.__doc__, 'feide_authn_level_add': cls.feide_authn_level_add.__doc__, 'feide_authn_level_remove': cls.feide_authn_level_remove.__doc__, 'feide_authn_level_list': cls.feide_authn_level_list.__doc__, }, } arg_help = { 'feide_service_name': ['service_name', 'Enter Feide service name'], 'feide_service_id': ['feide_id', 'Enter Feide service ID'], 'feide_service_confirm_remove': [ 'confirm', 'This will remove any authentication levels associated with ' 'this service. Continue? [y/n]' ], 'feide_authn_level': ['level', 'Enter authentication level'], 'feide_authn_entity_target': [ 'entity', 'Enter an existing entity', """Enter the entity as type:name, for example: 'group:admin-users' If only a name is entered, it will be assumed to be an account name. Supported types are: - 'person' (name of user => Person) - 'group' (name of group => Group) - 'id' (entity ID => any Person or Group) """ ], } return (group_help, command_help, arg_help) # feide service_add all_commands['feide_service_add'] = Command( ('feide', 'service_add'), Integer(help_ref='feide_service_id'), SimpleString(help_ref='feide_service_name'), perm_filter='is_superuser') def feide_service_add(self, operator, feide_id, service_name): """ Add a Feide service """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may add Feide services') if not feide_id.isdigit(): raise CerebrumError('Feide ID can only contain digits.') fse = FeideService(self.db) service_name = service_name.strip() name_error = fse.illegal_name(service_name) if name_error: raise CerebrumError(name_error) for service in fse.search(): if int(feide_id) == int(service['feide_id']): raise CerebrumError( 'A Feide service with that ID already exists') if service_name == service['name']: raise CerebrumError( 'A Feide service with that name already exists') fse.populate(feide_id, service_name) fse.write_db() return "Added Feide service '{}'".format(service_name) # feide service_remove all_commands['feide_service_remove'] = Command( ('feide', 'service_remove'), SimpleString(help_ref='feide_service_name'), YesNo(help_ref='feide_service_confirm_remove'), perm_filter='is_superuser') def feide_service_remove(self, operator, service_name, confirm): """ Remove a Feide service. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may remove Feide services') if not confirm: return 'No action taken.' service_name = service_name.strip() fse = self._find_service(service_name) fse.delete() fse.write_db() return "Removed Feide service '{}'".format(service_name) # feide service_list all_commands['feide_service_list'] = Command( ('feide', 'service_list'), fs=FormatSuggestion( '%-12d %-12d %s', ('service_id', 'feide_id', 'name'), hdr='%-12s %-12s %s' % ('Entity ID', 'Feide ID', 'Name')), perm_filter='is_superuser') def feide_service_list(self, operator): """ List Feide services. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may list Feide services') fse = FeideService(self.db) return map(dict, fse.search()) # feide authn_level_add all_commands['feide_authn_level_add'] = Command( ('feide', 'authn_level_add'), SimpleString(help_ref='feide_service_name'), SimpleString(help_ref='feide_authn_entity_target'), Integer(help_ref='feide_authn_level'), perm_filter='is_superuser') def feide_authn_level_add(self, operator, service_name, target, level): """ Add an authentication level for a given service and entity. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may add Feide authentication levels') if not level.isdigit() or int(level) not in (3, 4): raise CerebrumError('Authentication level must be 3 or 4') service_name = service_name.strip() fse = self._find_service(service_name) # Allow authentication levels for persons and groups entity = self.util.get_target(target, default_lookup='person', restrict_to=['Person', 'Group']) if entity.search_authn_level(service_id=fse.entity_id, entity_id=entity.entity_id, level=level): raise CerebrumError( 'Authentication level {} for {} for service {} ' 'already enabled'.format(level, target, service_name)) entity.add_authn_level(service_id=fse.entity_id, level=level) return 'Added authentication level {} for {} for {}'.format( level, target, service_name) # feide authn_level_remove all_commands['feide_authn_level_remove'] = Command( ('feide', 'authn_level_remove'), SimpleString(help_ref='feide_service_name'), SimpleString(help_ref='feide_authn_entity_target'), Integer(help_ref='feide_authn_level'), perm_filter='is_superuser') def feide_authn_level_remove(self, operator, service_name, target, level): """ Remove an authentication level for a given service and entity. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may remove Feide authentication levels') if not level.isdigit() or int(level) not in (3, 4): raise CerebrumError('Authentication level must be 3 or 4') service_name = service_name.strip() fse = self._find_service(service_name) # Allow authentication levels for persons and groups entity = self.util.get_target(target, default_lookup='person', restrict_to=['Person', 'Group']) if not entity.search_authn_level(service_id=fse.entity_id, entity_id=entity.entity_id, level=level): raise CerebrumError( 'No such authentication level {} for {} for service {}'.format( level, target, service_name)) entity.remove_authn_level(service_id=fse.entity_id, level=level) return 'Removed authentication level {} for {} for {}'.format( level, target, service_name) # feide authn_level_search all_commands['feide_authn_level_list'] = Command( ('feide', 'authn_level_list'), SimpleString(help_ref='feide_service_name'), fs=FormatSuggestion( '%-20s %-6d %s', ('service_name', 'level', 'entity'), hdr='%-20s %-6s %s' % ('Service', 'Level', 'Entity')), perm_filter='is_superuser') def feide_authn_level_list(self, operator, service_name): """ List all authentication levels for a service. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may list Feide authentication levels') service_name = service_name.strip() fse = self._find_service(service_name) en = Factory.get('Entity')(self.db) fsal = FeideServiceAuthnLevelMixin(self.db) result = [] for x in fsal.search_authn_level(service_id=fse.entity_id): try: en.clear() en.find(x['entity_id']) entity_type = str(self.const.map_const(en.entity_type)) entity_name = self._get_entity_name(en.entity_id, en.entity_type) entity = '{} {} (id:{:d})'.format(entity_type, entity_name, en.entity_id) except: entity = 'id:{}'.format(x['entity_id']) result.append({ 'service_name': service_name, 'level': x['level'], 'entity': entity }) return result
class BofhdExtension(BofhdCommonMethods): u""" Extends bofhd with a 'note' command group. """ all_commands = {} parent_commands = False authz = BofhdAuth @classmethod def get_help_strings(cls): group_help = { 'note': 'Entity note related commands', } command_help = { 'note': { 'note_show': 'Show notes associated with an entity', 'note_add': 'Add a new note to an entity', 'note_remove': 'Remove a note associated with an entity', }, } arg_help = { 'note_id': ['note_id', 'Enter note ID', 'Enter the ID of the note'], 'note_subject': ['note_subject', 'Enter subject', 'Enter the subject of the note'], 'note_description': ['note_description', 'Enter description', 'Enter the description of the note'], } return (group_help, command_help, arg_help) # # note show <subject-id> # all_commands['note_show'] = Command( ('note', 'show'), Id(help_ref='id:target:entity'), fs=FormatSuggestion([ ("%d note(s) found for %s:\n", ("notes_total", "entity_target")), ("Note #%d added by %s on %s:\n" "%s: %s\n", ("note_id", "creator", format_time("create_date"), "subject", "description")) ]), perm_filter='can_show_notes') def note_show(self, operator, entity_target): self.ba.can_show_notes(operator.get_entity_id()) entity = self.util.get_target( entity_target, default_lookup="account", restrict_to=[] ) enote = Note.EntityNote(self.db) enote.find(entity.entity_id) notes = enote.get_notes() result = [] if len(notes) is 0: return "No notes were found for %s" % (entity_target) else: result.append({ 'notes_total': len(notes), 'entity_target': entity_target, }) for note_row in notes: note = {} for key, value in note_row.items(): note[key] = value if key in ('subject', 'description') and len(value) is 0: note[key] = '<not set>' # translate creator_id to username acc = self._get_account(note_row['creator_id'], idtype='id') note['creator'] = acc.account_name result.append(note) return result # # note add <subject-id> <title> <contents> # all_commands['note_add'] = Command( ('note', 'add'), Id(help_ref='id:target:entity'), SimpleString(help_ref='note_subject'), SimpleString(help_ref='note_description'), perm_filter='can_add_notes') def note_add(self, operator, entity_target, subject, description): self.ba.can_add_notes(operator.get_entity_id()) if len(subject) > 70: raise CerebrumError( u"Subject field cannot be longer than 70 characters") if len(description) > 1024: raise CerebrumError( u"Description field cannot be longer than 1024 characters") entity = self.util.get_target(entity_target, restrict_to=[]) enote = Note.EntityNote(self.db) enote.find(entity.entity_id) note_id = enote.add_note(operator.get_entity_id(), subject, description) return "Note #%s was added to entity %s" % (note_id, entity_target) # # note remove <subject-id> <note-id> # all_commands['note_remove'] = Command( ('note', 'remove'), Id(help_ref='id:target:entity'), SimpleString(help_ref='note_id'), perm_filter='can_remove_notes') def note_remove(self, operator, entity_target, note_id): self.ba.can_remove_notes(operator.get_entity_id()) entity = self.util.get_target(entity_target, restrict_to=[]) enote = Note.EntityNote(self.db) enote.find(entity.entity_id) if not note_id.isdigit(): raise CerebrumError("Note ID must be a numeric value") for note_row in enote.get_notes(): if int(note_row['note_id']) is int(note_id): enote.delete_note(note_id) return "Note #%s associated with entity %s was removed" % ( note_id, entity_target) raise CerebrumError("Note #%s is not associated with entity %s" % ( note_id, entity_target))
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}
class BofhdExtension(BofhdCommandBase): """Commands used for managing and inspecting events.""" all_commands = {} parent_commands = False authz = BofhdAuth @classmethod def get_help_strings(cls): """Definition of the help text for event-related commands.""" group_help = { 'event': "Event related commands", } # The texts in command_help are automatically line-wrapped, and should # not contain \n command_help = { 'event': { 'event_stat': 'Show statistics about the events', 'event_info': 'Display an event', 'event_list': 'List locked and failed events', 'event_force': 'Force processing of a terminally failed event', 'event_force_all': ('Force processing of all failed events for a ' 'target system'), 'event_unlock': 'Unlock a previously locked event', 'event_delete': 'Delete an event', 'event_search': 'Search for events' }, } arg_help = { 'target_system': [ 'target_system', 'Target system (e.g. \'Exchange\')', 'Enter the target system for this operation' ], 'event_id': ['event_id', 'Event Id', 'The numerical identifier of an event'], 'search_pattern': [ 'search_pattern', 'Search pattern', 'Patterns that can be used:\n' ' id:0 Matches events where dest- or ' 'subject_entity is set to 0\n' ' type:spread:add Matches events of type spread:add\n' ' param:Joe Matches events where the string' ' "Joe" is found in the change params\n' ' target_system:Exchange Matches events for a target system\n' ' from_ts:2016-01-01 Matches events from after a ' 'timestamp\n' ' to_ts:2016-12-31 Matches events from before a ' 'timestamp\n' 'In combination, these patterns form a boolean AND expression.' '\nTimestamps can also be \'today\', \'yesterday\' or precise' ' to the second.' ] } return (group_help, command_help, arg_help) # Validate that the target system exists, and that the operator is # allowed to perform operations on it. def _validate_target_system(self, operator, target_sys): # TODO: Check for perms on target system. ts = self.const.TargetSystem(target_sys) try: # Provoke. See if it exists. int(ts) except Errors.NotFoundError: raise CerebrumError('No such target system: {}'.format(target_sys)) return ts # Convert dictionary entries known to contain contant codes, # to human-readable text def _make_constants_human_readable(self, data): constant_keys = [ 'spread', 'entity_type', 'code', 'affiliation', 'src', 'name_variant', 'action', 'level' ] try: for key in constant_keys: if key in data: value = self.const.human2constant(data[key]) if value: data[key] = str(value) except TypeError: pass return data # event stat all_commands['event_stat'] = Command(( 'event', 'stat', ), TargetSystem(), fs=FormatSuggestion([ ( 'Total failed: %d\n' 'Total locked: %d\n' 'Total : %d', ( 't_failed', 't_locked', 'total', ), ), ]), perm_filter='is_postmaster') def event_stat(self, operator, target_sys): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, target_sys) fail_limit = eventconf.CONFIG[str(ts)]['fail_limit'] return self.db.get_target_stats(ts, fail_limit) # event list all_commands['event_list'] = Command(( 'event', 'list', ), TargetSystem(), SimpleString(optional=True), fs=FormatSuggestion( '%-8d %-28s %-25s %d', ( 'id', 'type', 'taken', 'failed', ), hdr='%-8s %-28s %-25s %s' % ( 'Id', 'Type', 'Taken', 'Failed', ), ), perm_filter='is_postmaster') def event_list(self, operator, target_sys, args='failed'): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, target_sys) r = [] # TODO: Check auth on target-system # Remove perm_filter when this is implemented? if args == 'failed': fail_limit = eventconf.CONFIG[str(ts)]['fail_limit'] locked = True elif args == 'full': fail_limit = None locked = False else: return [] for ev in self.db.get_failed_and_locked_events(target_system=ts, fail_limit=fail_limit, locked=locked): r += [{ 'id': ev['event_id'], 'type': str(self.const.map_const(ev['event_type'])), 'taken': str(ev['taken_time']).replace(' ', '_'), 'failed': ev['failed'] }] return r # event force_all all_commands['event_force_all'] = Command( ( 'event', 'force_all', ), TargetSystem(), fs=FormatSuggestion('Forced %d events', ('rowcount', )), perm_filter='is_postmaster') def event_force_all(self, operator, ts): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, ts) rowcount = self.db.reset_failed_counts_for_target_system(ts) return {'rowcount': rowcount} # event force all_commands['event_force'] = Command( ( 'event', 'force', ), EventId(), fs=FormatSuggestion('Forcing %s', ('state', )), perm_filter='is_postmaster') def event_force(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.reset_failed_count(event_id) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # event unlock all_commands['event_unlock'] = Command( ( 'event', 'unlock', ), EventId(), fs=FormatSuggestion('Unlock %s', ('state', )), perm_filter='is_postmaster') def event_unlock(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.release_event(event_id, increment=False) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # event delete all_commands['event_delete'] = Command( ( 'event', 'delete', ), EventId(), fs=FormatSuggestion('Deletion %s', ('state', )), perm_filter='is_postmaster') def event_delete(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.remove_event(event_id) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # event info all_commands['event_info'] = Command( ( 'event', 'info', ), EventId(), fs=FormatSuggestion( 'Event ID: %d\n' 'Event type: %s\n' 'Target system: %s\n' 'Failed attempts: %d\n' 'Time added: %s\n' 'Time taken: %s\n' 'Subject entity: %s\n' 'Destination entity: %s\n' 'Parameters: %s', ('event_id', 'event_type', 'target_system', 'failed', 'tstamp', 'taken_time', 'subject_entity', 'dest_entity', 'change_params')), perm_filter='is_postmaster') def event_info(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: ev = self.db.get_event(event_id) except Errors.NotFoundError: raise CerebrumError('Error: No such event exists!') # For certain keys, convert constants to human-readable representations change_params = ev['change_params'] if change_params: change_params = pickle.loads(ev['change_params']) change_params = self._make_constants_human_readable(change_params) ret = { 'event_id': ev['event_id'], 'event_type': str(self.const.map_const(ev['event_type'])), 'target_system': str(self.const.map_const(ev['target_system'])), 'failed': ev['failed'], 'tstamp': str(ev['tstamp']), 'taken_time': str(ev['taken_time']) if ev['taken_time'] else '<not set>', 'subject_entity': str(ev['subject_entity']) if ev['subject_entity'] else '<not set>', 'dest_entity': str(ev['dest_entity']) if ev['dest_entity'] else '<not set>', 'change_params': repr(change_params) if change_params else '<not set>' } en = Factory.get('Entity')(self.db) # Look up types and names for subject and destination entities for key in ('subject_entity', 'dest_entity'): if ev[key]: try: en.clear() en.find(ev[key]) entity_type = str(self.const.map_const(en.entity_type)) entity_name = self._get_entity_name( en.entity_id, en.entity_type) ret[key] = '{} {} (id:{:d})'.format( entity_type, entity_name, en.entity_id) except: pass return ret # event search all_commands['event_search'] = Command( ( 'event', 'search', ), SimpleString(repeat=True, help_ref='search_pattern'), fs=FormatSuggestion( '%-8d %-35s %-15s %-15s %-25s %-6d %s', ('id', 'type', 'subject_type', 'dest_type', 'taken', 'failed', 'params'), hdr='%-8s %-35s %-15s %-15s %-25s %-6s %s' % ('Id', 'Type', 'SubjectType', 'DestinationType', 'Taken', 'Failed', 'Params'), ), perm_filter='is_postmaster') def event_search(self, operator, *args): """Search for events in the database. :param str search_str: A pattern to search for. """ if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') # TODO: Fetch an ACL of which target systems can be searched by this # TODO: Make the param-search handle spaces and stuff? params = {} # Parse search patterns for arg in args: try: key, value = arg.split(':', 1) except ValueError: # Ignore argument continue if key == 'type': try: cat, typ = value.split(':') except ValueError: raise CerebrumError('Search pattern incomplete') try: type_code = int(self.const.ChangeType(cat, typ)) except Errors.NotFoundError: raise CerebrumError('EventType does not exist') params['type'] = type_code elif key == 'target_system': ts = self._validate_target_system(operator, value) params['target_system'] = ts else: params[key] = value # Raise if we do not have any search patterns if not params: raise CerebrumError('Must specify search pattern.') # Fetch matching event ids try: event_ids = self.db.search_events(**params) except TypeError: # If the user spells an argument wrong, we tell them that they have # done so. They can always type '?' at the pattern-prompt to get # help. raise CerebrumError('Invalid arguments') except self.db.DataError as e: # E.g. bogus timestamp message = e.args[0].split("\n")[0] raise CerebrumError('Database does not approve: ' + message) # Fetch information about the event ids, and present it to the user. r = [] for event_id in event_ids: ev = self.db.get_event(event_id['event_id']) try: types = self.db.get_event_target_type(event_id['event_id']) except Errors.NotFoundError: # If we wind up here, both the subject- or destination-entity # has been deleted, or the event does not carry information # about a subject- or destination-entity. types = [] change_params = ev['change_params'] if ev['change_params']: change_params = pickle.loads(ev['change_params']) change_params = self._make_constants_human_readable( change_params) ret = { 'id': ev['event_id'], 'type': str(self.const.map_const(ev['event_type'])), 'taken': str(ev['taken_time']).replace(' ', '_'), 'failed': ev['failed'], 'params': repr(change_params), 'dest_type': None, 'subject_type': None, } if 'dest_type' in types: ret['dest_type'] = str(self.const.map_const( types['dest_type'])) if 'subject_type' in types: ret['subject_type'] = str( self.const.map_const(types['subject_type'])) r.append(ret) return r
class BofhdExtension(BofhdCommandBase): """Commands used for managing and inspecting events.""" all_commands = {} parent_commands = False authz = BofhdAuth @classmethod def get_help_strings(cls): """Definition of the help text for event-related commands.""" _, _, args = get_help_strings() return merge_help_strings( ({}, {}, args), (HELP_EVENT_GROUP, HELP_EVENT_CMDS, HELP_EVENT_ARGS)) def _validate_target_system(self, operator, target_sys): """Validate that the target system exists.""" # TODO: Check for perms on target system. ts = self.const.TargetSystem(target_sys) try: # Provoke. See if it exists. int(ts) except Errors.NotFoundError: raise CerebrumError('No such target system: %r' % target_sys) return ts def _make_constants_human_readable(self, data): """Convert dictionary entries known to contain contant codes, to human-readable text.""" constant_keys = [ 'spread', 'entity_type', 'code', 'affiliation', 'src', 'name_variant', 'action', 'level' ] try: for key in constant_keys: if key in data: value = self.const.human2constant(data[key]) if value: data[key] = six.text_type(value) except TypeError: pass return data def _parse_search_params(self, *args): """Convert string search pattern to a dict.""" # TODO: Make the param-search handle spaces and stuff? params = defaultdict(list) for arg in args: try: key, value = arg.split(':', 1) except ValueError: # Ignore argument continue if key == 'type': try: cat, typ = value.split(':') except ValueError: raise CerebrumError('Search pattern incomplete') try: type_code = int(self.clconst.ChangeType(cat, typ)) except Errors.NotFoundError: raise CerebrumError('EventType does not exist') params['type'].append(type_code) elif key == 'is_taken': params[key] = self._get_boolean(value) else: params[key] = value return params def _search_events(self, **params): """Find rows matching search params.""" try: return self.db.search_events(**params) except TypeError: # If the user spells an argument wrong, we tell them that they have # done so. They can always type '?' at the pattern-prompt to get # help. raise CerebrumError('Invalid arguments') except self.db.DataError as e: # E.g. bogus timestamp message = e.args[0].split("\n")[0] raise CerebrumError('Database does not approve: ' + message) # # event stat # all_commands['event_stat'] = Command(( 'event', 'stat', ), TargetSystem(), fs=FormatSuggestion([ ( 'Total failed: %d\n' 'Total locked: %d\n' 'Total : %d', ( 't_failed', 't_locked', 'total', ), ), ]), perm_filter='is_postmaster') def event_stat(self, operator, target_sys): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, target_sys) fail_limit = eventconf.CONFIG[six.text_type(ts)]['fail_limit'] return self.db.get_target_stats(ts, fail_limit) # # event list # all_commands['event_list'] = Command( ( 'event', 'list', ), TargetSystem(), SimpleString(help_ref='event_list_filter', optional=True), fs=FormatSuggestion( '%-8d %-35s %-22s %d', ( 'id', 'type', format_time('taken'), 'failed', ), hdr='%-8s %-35s %-22s %s' % ( 'Id', 'Type', 'Taken', 'Failed', ), ), perm_filter='is_postmaster') def event_list(self, operator, target_sys, args='failed'): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, target_sys) r = [] # TODO: Check auth on target-system # Remove perm_filter when this is implemented? if args == 'failed': fail_limit = eventconf.CONFIG[six.text_type(ts)]['fail_limit'] locked = True elif args == 'full': fail_limit = None locked = False else: return [] for ev in self.db.get_failed_and_locked_events(target_system=ts, fail_limit=fail_limit, locked=locked): r.append({ 'id': ev['event_id'], 'type': six.text_type(self.clconst.map_const(ev['event_type'])), 'taken': ev['taken_time'], 'failed': ev['failed'] }) return r # # event force_all # all_commands['event_force_all'] = Command( ( 'event', 'force_all', ), TargetSystem(), fs=FormatSuggestion('Forced %d events', ('rowcount', )), perm_filter='is_postmaster') def event_force_all(self, operator, ts): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') ts = self._validate_target_system(operator, ts) rowcount = self.db.reset_failed_counts_for_target_system(ts) return {'rowcount': rowcount} # # event force # all_commands['event_force'] = Command( ( 'event', 'force', ), EventId(), fs=FormatSuggestion('Forcing %s', ('state', )), perm_filter='is_postmaster') def event_force(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.reset_failed_count(event_id) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # # event unlock # all_commands['event_unlock'] = Command( ( 'event', 'unlock', ), EventId(), fs=FormatSuggestion('Unlock %s', ('state', )), perm_filter='is_postmaster') def event_unlock(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.release_event(event_id, increment=False) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # event delete all_commands['event_delete'] = Command( ( 'event', 'delete', ), EventId(), fs=FormatSuggestion('Deletion %s', ('state', )), perm_filter='is_postmaster') def event_delete(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: self.db.remove_event(event_id) state = True except Errors.NotFoundError: state = False return {'state': 'failed' if not state else 'succeeded'} # event delete_where all_commands['event_delete_where'] = Command( ( 'event', 'delete_where', ), TargetSystem(), SimpleString(repeat=True, help_ref='search_pattern'), fs=FormatSuggestion( 'Deleted %s of %s matching events (%s failed/vanished)', ('success', 'total', 'failed')), perm_filter='is_postmaster') def event_delete_where(self, operator, target_sys, *args): """Delete events matching a search query. :param str target_sys: Target system to search :param str args: Pattern(s) to search for. """ if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') # TODO: Fetch an ACL of which target systems can be searched by this ts = self._validate_target_system(operator, target_sys) params = self._parse_search_params(*args) if not params: raise CerebrumError('Must specify search pattern.') params['target_system'] = ts event_ids = [row['event_id'] for row in self._search_events(**params)] stats = {} stats['total'] = len(event_ids) stats['success'] = stats['failed'] = 0 for event_id in event_ids: try: self.db.remove_event(event_id) stats['success'] += 1 except Errors.NotFoundError: stats['failed'] += 1 return stats # event unlock_where all_commands['event_unlock_where'] = Command( ( 'event', 'unlock_where', ), TargetSystem(), SimpleString(repeat=True, help_ref='search_pattern'), fs=FormatSuggestion( 'Unlocked %d of %d matching events (%d failed/vanished)', ('success', 'total', 'failed')), perm_filter='is_postmaster') def event_unlock_where(self, operator, target_sys, *args): """Unlock events matching a search query. :param str target_sys: Target system to search :param str args: Pattern(s) to search for. """ if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') # TODO: Fetch an ACL of which target systems can be searched by this ts = self._validate_target_system(operator, target_sys) params = self._parse_search_params(*args) if not params: raise CerebrumError('Must specify search pattern.') params['is_taken'] = True params['target_system'] = ts event_ids = [row['event_id'] for row in self._search_events(**params)] stats = {} stats['total'] = len(event_ids) stats['success'] = stats['failed'] = 0 for event_id in event_ids: try: self.db.release_event(event_id, increment=False) stats['success'] += 1 except Errors.NotFoundError: stats['failed'] += 1 return stats # event info all_commands['event_info'] = Command( ( 'event', 'info', ), EventId(), fs=FormatSuggestion( 'Event ID: %d\n' 'Event type: %s\n' 'Target system: %s\n' 'Failed attempts: %d\n' 'Time added: %s\n' 'Time taken: %s\n' 'Subject entity: %s\n' 'Destination entity: %s\n' 'Parameters: %s', ('event_id', 'event_type', 'target_system', 'failed', format_time('tstamp'), format_time('taken_time'), 'subject_entity', 'dest_entity', 'change_params')), perm_filter='is_postmaster') def event_info(self, operator, event_id): if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') try: ev = self.db.get_event(event_id) except ValueError: raise CerebrumError("Error: Event id must be an integer") except Errors.NotFoundError: raise CerebrumError('Error: No such event exists!') # For certain keys, convert constants to human-readable representations change_params = ev['change_params'] if change_params: change_params = json.loads(ev['change_params']) change_params = self._make_constants_human_readable(change_params) change_params = repr(change_params) else: change_params = None ret = { 'event_id': ev['event_id'], 'event_type': six.text_type(self.clconst.map_const(ev['event_type'])), 'target_system': six.text_type(self.const.map_const(ev['target_system'])), 'failed': ev['failed'], 'tstamp': ev['tstamp'], 'taken_time': ev['taken_time'], 'subject_entity': ev['subject_entity'], 'dest_entity': ev['dest_entity'], 'change_params': change_params, } en = Factory.get('Entity')(self.db) # Look up types and names for subject and destination entities for key in ('subject_entity', 'dest_entity'): if ev[key]: try: en.clear() en.find(ev[key]) entity_type = six.text_type( self.const.map_const(en.entity_type)) entity_name = self._get_entity_name( en.entity_id, en.entity_type) ret[key] = '{} {} (id:{:d})'.format( entity_type, entity_name, en.entity_id) except Exception: pass return ret # # event search # all_commands['event_search'] = Command( ( 'event', 'search', ), TargetSystem(), SimpleString(repeat=True, help_ref='search_pattern'), fs=FormatSuggestion( '%-8d %-35s %-15s %-15s %-22s %-6d %s', ('id', 'type', 'subject_type', 'dest_type', format_time('taken'), 'failed', 'params'), hdr='%-8s %-35s %-15s %-15s %-22s %-6s %s' % ('Id', 'Type', 'SubjectType', 'DestinationType', 'Taken', 'Failed', 'Params'), ), perm_filter='is_postmaster') def event_search(self, operator, target_sys, *args): """Search for events in the database. :param str target_sys: Target system to search :param str args: Pattern(s) to search for. """ if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('No access to event') # TODO: Fetch an ACL of which target systems can be searched by this ts = self._validate_target_system(operator, target_sys) params = self._parse_search_params(*args) if not params: raise CerebrumError('Must specify search pattern.') params['target_system'] = ts event_ids = self._search_events(**params) # Fetch information about the event ids, and present it to the user. r = [] for event_id in event_ids: ev = self.db.get_event(event_id['event_id']) try: types = self.db.get_event_target_type(event_id['event_id']) except Errors.NotFoundError: # If we wind up here, both the subject- or destination-entity # has been deleted, or the event does not carry information # about a subject- or destination-entity. types = [] change_params = ev['change_params'] if ev['change_params']: change_params = json.loads(ev['change_params']) change_params = self._make_constants_human_readable( change_params) ret = { 'id': ev['event_id'], 'type': six.text_type(self.clconst.map_const(ev['event_type'])), 'taken': ev['taken_time'], 'failed': ev['failed'], 'params': repr(change_params), 'dest_type': None, 'subject_type': None, } if 'dest_type' in types: ret['dest_type'] = six.text_type( self.const.map_const(types['dest_type'])) if 'subject_type' in types: ret['subject_type'] = six.text_type( self.const.map_const(types['subject_type'])) r.append(ret) return r
class BofhdExtension(BofhdCommonMethods): all_commands = {} authz = BofhdAuth @classmethod def get_help_strings(cls): """Get help strings.""" # No GROUP_HELP, we'll just guess that 'user' and 'misc' exists # already. # The help structure doesn't really allow command groups to come from # different bofhd-extensions, so if we were to implement texts for # these groups, they might override texts defined elsewhere... return ({}, CMD_HELP, CMD_ARGS) def _get_phone_number(self, person_id, phone_types): """Search through a person's contact info and return the first found info value as defined by the given types and source systems. @type person_id: integer @param person_id: Entity ID of the person @type phone_types: list @param phone_types: list containing pairs of the source system and contact type to look for, i.e. on the form: (_AuthoritativeSystemCode, _ContactInfoCode) """ person = self._get_person('entity_id', person_id) for sys, type in phone_types: for row in person.get_contact_info(source=sys, type=type): return row['contact_value'] return None def _select_sms_number(self, account_name): """Find the best matching mobile number for SMS. Search through an account owners contact info and return the best match according to cereconf.SMS_NUMBER_SELECTOR :param str account_name: account name :return str: mobile phone number """ person = self._get_person('account_name', account_name) try: spec = map(lambda (s, t): (self.const.human2constant(s), self.const.human2constant(t)), cereconf.SMS_NUMBER_SELECTOR) mobile = person.sort_contact_info(spec, person.get_contact_info()) person_in_systems = [int(af['source_system']) for af in person.list_affiliations( person_id=person.entity_id)] return filter(lambda x: x['source_system'] in person_in_systems, mobile)[0]['contact_value'] except IndexError: raise CerebrumError('No applicable phone number for %r' % account_name) # # user send_welcome_sms <accountname> [<mobile override>] # all_commands['user_send_welcome_sms'] = Command( ("user", "send_welcome_sms"), AccountName(help_ref="account_name", repeat=False), Mobile(help_ref='welcome_mobile', optional=True), fs=FormatSuggestion([('Ok, message sent to %s', ('mobile',)), ]), perm_filter='can_send_welcome_sms') def user_send_welcome_sms(self, operator, username, mobile=None): """Send a (new) welcome SMS to a user. Optional mobile override, if what's registered in Cerebrum is wrong or missing. Override must be permitted in the cereconf setting BOFHD_ALLOW_MANUAL_MOBILE. """ sms = SMSSender(logger=self.logger) account = self._get_account(username) # Access Control self.ba.can_send_welcome_sms(operator.get_entity_id()) # Ensure allowed to specify a phone number if not cereconf.BOFHD_ALLOW_MANUAL_MOBILE and mobile: raise CerebrumError('Not allowed to specify number') # Ensure proper formatted phone number if mobile and not (len(mobile) == 8 and mobile.isdigit()): raise CerebrumError('Invalid phone number, must be 8 digits') # Ensure proper account if account.is_deleted(): raise CerebrumError("User is deleted") if account.is_expired(): raise CerebrumError("User is expired") if account.owner_type != self.const.entity_person: raise CerebrumError("User is not a personal account") # Look up the mobile number if not mobile: phone_types = [(self.const.system_sap, self.const.contact_private_mobile), (self.const.system_sap, self.const.contact_mobile_phone), (self.const.system_fs, self.const.contact_mobile_phone)] mobile = self._get_phone_number(account.owner_id, phone_types) if not mobile: raise CerebrumError("No mobile phone number for %r" % username) # Get primary e-mail address, if it exists mailaddr = '' try: mailaddr = account.get_primary_mailaddress() except: pass # NOTE: There's no need to supply the 'email' entry at the moment, # but contrib/no/send_welcome_sms.py does it as well # TODO: The whole templating system is getting re-worked, ~tgk can deal # with this... message = cereconf.AUTOADMIN_WELCOME_SMS % {"username": username, "studentnumber": '', "email": mailaddr} if not sms(mobile, message): raise CerebrumError("Could not send SMS to %r" % mobile) # Set sent sms welcome sent-trait, so that it will be ignored by the # scheduled job for sending welcome-sms. try: account.delete_trait(self.const.trait_sms_welcome) except Errors.NotFoundError: pass finally: account.populate_trait(code=self.const.trait_sms_welcome, date=DateTime.now()) account.write_db() return {'mobile': mobile} # # misc sms_password <username> [lang] # all_commands['misc_sms_password'] = Command( ('misc', 'sms_password'), AccountName(help_ref="account_name", repeat=False), SimpleString(help_ref='sms_pass_lang', repeat=False, optional=True, default='no'), fs=FormatSuggestion('Password sent to %s.', ('number',)), perm_filter='is_superuser') def misc_sms_password(self, operator, account_name, language='no'): """Send last password set for account in cache.""" if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Only superusers may send passwords by SMS") # Select last password change state entry for the victim try: state = filter( lambda k: account_name == k.get('account_id'), filter(lambda e: e.get('operation') == 'user_passwd', self._get_cached_passwords(operator)))[-1] except IndexError: raise CerebrumError('No password for %r in session' % account_name) mobile = self._select_sms_number(account_name) # Load and fill template for chosen language # TODO: The whole templating system is getting re-worked, ~tgk can deal # with this... try: with io.open( os.path.join( cereconf.TEMPLATE_DIR, 'password_sms_{}.template'.format(language)), 'r', encoding='utf-8') as f: msg = f.read().format( account_name=account_name, password=state.get('password')) except IOError: raise CerebrumError( 'Could not load template for language {}'.format(language)) # Maybe send SMS if getattr(cereconf, 'SMS_DISABLE', False): self.logger.info( 'SMS disabled in cereconf, would have ' 'sent password to %r', mobile) else: sms = SMSSender(logger=self.logger) if not sms(mobile, msg): raise CerebrumError('Unable to send message to %r' % mobile) return {'number': mobile} # # misc sms_message <username> <message> # all_commands['misc_sms_message'] = Command( ('misc', 'sms_message'), AccountName(help_ref='account_name'), SMSString(help_ref='sms_message', repeat=False), fs=FormatSuggestion('Message sent to %s.', ('number',)), perm_filter='is_superuser') def misc_sms_message(self, operator, account_name, message): """ Sends SMS message(s) """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may send messages by SMS') mobile = self._select_sms_number(account_name) # Send SMS if getattr(cereconf, 'SMS_DISABLE', False): self.logger.info( 'SMS disabled in cereconf, would have ' 'sent message to %r', mobile) else: sms = SMSSender(logger=self.logger) if not sms(mobile, message): raise CerebrumError('Unable to send message to %r' % mobile) return {'number': mobile}
class HostPolicyBofhdExtension(BofhdCommandBase): u"""Class with commands for manipulating host policies. """ all_commands = {} authz = HostPolicyBofhdAuth def __init__(self, *args, **kwargs): super(HostPolicyBofhdExtension, self).__init__(*args, **kwargs) # TODO: don't know where to get the zone setting from self.default_zone = self.const.DnsZone( getattr(cereconf, 'DNS_DEFAULT_ZONE', 'uio')) @classmethod def get_help_strings(cls): """Help strings are used by jbofh to give users explanations for groups of commands, commands and all command arguments (parameters). The arg_help's keys are referencing to either Parameters' _help_ref (TODO: or its _type in addition?)""" return merge_help_strings( ({}, {}, HELP_DNS_ARGS), (HELP_POLICY_GROUP, HELP_POLICY_CMDS, HELP_POLICY_ARGS)) def _get_component(self, comp_id, comp_class=PolicyComponent): """Helper method for getting a policy, or a given subtype.""" comp = comp_class(self.db) try: if type(comp_id) == int or comp_id.isdigit(): comp.find(comp_id) else: comp.find_by_name(comp_id) except Errors.NotFoundError: if comp_class == Atom: raise CerebrumError("Couldn't find atom with id=%r" % comp_id) elif comp_class == Role: raise CerebrumError("Couldn't find role with id=%r" % comp_id) raise CerebrumError("Couldn't find policy with id=%r" % comp_id) return comp def _get_atom(self, atom_id): """Helper method for getting an atom.""" return self._get_component(atom_id, Atom) def _get_role(self, role_id): """Helper method for getting a role.""" return self._get_component(role_id, Role) def _get_host(self, host_id): """Helper method for getting the DnsOwner for the given host ID, which can either be an IP address, an A record or a CName alias.""" finder = Find(self.db, self.default_zone) tmp = host_id.split(".") if host_id.find(":") == -1 and tmp[-1].isdigit(): # host_id is an IP owner_id = finder.find_target_by_parsing(host_id, IP_NUMBER) else: owner_id = finder.find_target_by_parsing(host_id, DNS_OWNER) # Check if it is a Cname, if so: update the owner_id try: cname_record = CNameRecord(self.db) cname_record.find_by_cname_owner_id(owner_id) owner_id = cname_record.target_owner_id except Errors.NotFoundError: pass dns_owner = DnsOwner.DnsOwner(self.db) try: dns_owner.find(owner_id) except Errors.NotFoundError: raise CerebrumError('Unknown host: %r' % host_id) return dns_owner def _check_if_unused(self, comp): """Check if component is unused, i.e. not in any relationship or used as a policy for hosts. If it is in use, a CerebrumError is raised, telling where it is in use. Note that only one of the types of "usage" is explained.""" tmp = tuple( row['dns_owner_name'] for row in comp.search_hostpolicies(policy_id=comp.entity_id)) if tmp: raise CerebrumError("Policy is in use as policy for: %s" % ', '.join(tmp)) tmp = tuple(row['source_name'] for row in comp.search_relations(target_id=comp.entity_id)) if tmp: raise CerebrumError("Policy is used as target for: %s" % ', '.join(tmp)) tmp = tuple(row['target_name'] for row in comp.search_relations(source_id=comp.entity_id)) if tmp: raise CerebrumError("Policy is used as source for: %s" % ', '.join(tmp)) def _parse_filters(self, input, filters, default_filter=NotSet, default_value=NotSet, separator=',', type_sep=':'): """Parse an input string with different filters and return a dict with the different filters set, according to the set options. CerebrumErrors are raise in case of invalid input, with explanations to what have failed. The input string must define filters on the form: name1:pattern1,name2:pattern2,... the filters are separated by L{separator} (default: ','), and each filter has a name and a value, separated by L{type_sep} (default: ':'). The L{filters} is the dict that defines the available filters. Errors are raised if the input contains other types of filters. Example of filters: 'name': str 'desc': str 'spread': _is_spread_valid 'expired': _parse_date The filters' values are callbacks to a method that should validate and might reformat the input before it's returned. If a callback raises an error, a CerebrumError is given back to the user. If an input filter does not specify its filter type, the one defined in L{default_filter} is used - which should match a key in L{filters}. If L{default_value} is set, this value will be put in all defined filters that aren't specified in the input. """ if default_filter is not NotSet and default_filter not in filters: raise RuntimeError('Default filter not specified in the filters') if not input or input == "": raise CerebrumError("No filter specified") patterns = {} for rule in input.split(separator): rule = rule.strip() if rule.find(":") != -1: type, pattern = rule.split(type_sep, 1) elif default_filter is not NotSet: # the first defined filter is the default one type = default_filter pattern = rule else: raise CerebrumError('Filter type not specified for: %r' % rule) type, pattern = type.strip(), pattern.strip() if type not in filters: raise CerebrumError("Unknown filter type: %r" % type) if filters[type] is None: patterns[type] = pattern else: # call callback function: # Callbacks should only raise CerebrumErrors, which can be # raised directly. Everything else is bugs and should be # raised. patterns[type] = filters[type](pattern) # fill in with default values if default_value is not NotSet: for f in filters: if f not in patterns: patterns[f] = default_value return patterns def _parse_create_date_range(self, date, separator='--'): """Parse a string with a date range and return a tuple of length two with DateTime objects, or None, if range is missing. The format has the form: YYYY-MM-DD--YYYY-MM-DD where the end date is optional, and would then default to None. The main difference between this method and bofhd_uio_cmds' method _parse_date_from_to is that if only only one date is given, this is considered the start date and not the end date. In addition we differ between not set dates and dates that is explicitly set to None. Dates that have not been specified are set to NotSet, but dates that have explicitly set to nothing returns None. Examples: YYYY-MM-DD returns (<start>, NotSet) YYYY-MM-DD-- returns (<start>, None) --YYYY-MM-DD returns (None, <end>) YYYY-MM-DD--YYYY-MM-DD returns (<start>, <end>) '' (empty string) returns (NotSet, NotSet) """ date_start = date_end = NotSet if date: tmp = date.split(separator) if len(tmp) == 2: date_start = date_end = None if tmp[0]: # string could start with the separator date_start = self._parse_date(tmp[0]) if tmp[1]: # string could end with separator date_end = self._parse_date(tmp[1]) elif len(tmp) == 1: date_start = self._parse_date(date) else: raise CerebrumError("Incorrect date specification: %r" % date) return (date_start, date_end) # TODO: we miss functionality for setting mutex relationships # # policy atom_create # all_commands['policy_atom_create'] = Command( ('policy', 'atom_create'), AtomName(), Description(), Foundation(), FoundationDate(optional=True), perm_filter='is_dns_superuser') def policy_atom_create(self, operator, name, description, foundation, foundation_date=None): """Adds a new atom and its data. It can only consist of lowercased, alpha numrice characters and -. """ self.ba.assert_dns_superuser(operator.get_entity_id()) atom = Atom(self.db) # validate data tmp = atom.illegal_attr(description) if tmp: raise CerebrumError('Illegal description: %r' % tmp) tmp = atom.illegal_attr(foundation) if tmp: raise CerebrumError('Illegal foundation: %r' % tmp) foundation_date = self._parse_date(foundation_date) # check that name isn't already in use try: self._get_component(name) except CerebrumError: pass else: raise CerebrumError('A policy already exists with name: %r' % name) atom.populate(name, description, foundation, foundation_date) atom.write_db() return "New atom %s created" % atom.component_name # # policy atom_delete # all_commands['policy_atom_delete'] = Command( ('policy', 'atom_delete'), AtomId(), perm_filter='is_dns_superuser') def policy_atom_delete(self, operator, atom_id): """Delete an atom. Try to delete an atom if it hasn't been used in any policy or relationship. """ self.ba.assert_dns_superuser(operator.get_entity_id()) atom = self._get_atom(atom_id) self._check_if_unused(atom) # will raise CerebrumError name = atom.component_name atom.delete() atom.write_db() return "Atom %s deleted" % name # # policy role_create # all_commands['policy_role_create'] = Command( ('policy', 'role_create'), RoleName(), Description(), Foundation(), FoundationDate(optional=True), perm_filter='is_dns_superuser') def policy_role_create(self, operator, name, description, foundation, foundation_date=None): """Adds a new role and its data. It can only consist of lowercased, alpha numrice characters and -. """ self.ba.assert_dns_superuser(operator.get_entity_id()) role = Role(self.db) # validate data tmp = role.illegal_attr(description) if tmp: raise CerebrumError('Illegal description: %r' % tmp) tmp = role.illegal_attr(foundation) if tmp: raise CerebrumError('Illegal foundation: %r' % tmp) foundation_date = self._parse_date(foundation_date) # check that name isn't already in use try: self._get_component(name) except CerebrumError: pass else: raise CerebrumError('A policy already exists with name: %r' % name) role.populate(name, description, foundation, foundation_date) role.write_db() return "New role %s created" % role.component_name # # policy role_delete # all_commands['policy_role_delete'] = Command( ('policy', 'role_delete'), RoleId(), perm_filter='is_dns_superuser') def policy_role_delete(self, operator, role_id): """Delete a role. Try to delete a given role if it's not in any relationship, or is in any policy. """ self.ba.assert_dns_superuser(operator.get_entity_id()) role = self._get_role(role_id) # Check if policy is in use anywhere. The method will raise # CerebrumErrors if that's the case: self._check_if_unused(role) name = role.component_name role.delete() role.write_db() return "Role %s deleted" % name # # policy rename # all_commands['policy_rename'] = Command(('policy', 'rename'), PolicyId(), PolicyName(), perm_filter='is_dns_superuser') def policy_rename(self, operator, policy_id, name): """Rename an existing policy, if the name is not already taken.""" self.ba.assert_dns_superuser(operator.get_entity_id()) policy = self._get_component(policy_id) # check if name is already taken try: self._get_component(name) except CerebrumError: pass else: raise CerebrumError('New name %r is in use' % name) old_name = policy.component_name policy.component_name = name policy.write_db() return "Policy %s renamed to %s" % (old_name, name) # # policy set_description # all_commands['policy_set_description'] = Command( ('policy', 'set_description'), PolicyId(), Description(), perm_filter='is_dns_superuser') def policy_set_description(self, operator, policy_id, description): """Update the description of an existing policy.""" self.ba.assert_dns_superuser(operator.get_entity_id()) policy = self._get_component(policy_id) policy.description = description policy.write_db() return "Description updated for %s" % policy.component_name # # policy set_foundation # all_commands['policy_set_foundation'] = Command( ('policy', 'set_foundation'), PolicyId(), Foundation(), FoundationDate(optional=True), perm_filter='is_dns_superuser') def policy_set_foundation(self, operator, policy_id, foundation, date=None): """Update the foundation data of an existing policy.""" self.ba.assert_dns_superuser(operator.get_entity_id()) policy = self._get_component(policy_id) policy.foundation = foundation if date: policy.foundation_date = self._parse_date(date) policy.write_db() return "Foundation updated for %s" % policy.component_name # # policy add_member # all_commands['policy_add_member'] = Command( ('policy', 'add_member'), RoleId(help_ref='role_source'), PolicyId(help_ref='policy_target'), perm_filter='is_dns_superuser') def policy_add_member(self, operator, role_id, member_id): """Try to add a given policy as a member of a role.""" self.ba.assert_dns_superuser(operator.get_entity_id()) try: role = self._get_role(role_id) except CerebrumError as e: # check if it is an atom, and give better feedback try: self._get_atom(role_id) except CerebrumError: raise e raise CerebrumError("Atoms can't have members") member = self._get_component(member_id) if role.entity_id == member.entity_id: raise CerebrumError("Can't add a role to itself") # Check if already a member for row in role.search_relations( source_id=role.entity_id, relationship_code=self.const.hostpolicy_contains, indirect_relations=True): if row['target_id'] == member.entity_id: raise CerebrumError("%s already member of %s (through %s)" % (member.component_name, role.component_name, row['source_name'])) try: role.add_relationship(self.const.hostpolicy_contains, member.entity_id) except Errors.ProgrammingError as e: # The relationship were not accepted, give the user an explanation # of why. # TODO: need to check for mutex relationships! # Check if member is source in the relationship for row in role.search_relations( source_id=member.entity_id, relationship_code=self.const.hostpolicy_contains, indirect_relations=True): if row['target_id'] == role.entity_id: raise CerebrumError( "%s is already a parent for %s" " (through %s)" % (member.component_name, role.component_name, row['source_name'])) # if we got here, we weren't able to explain what is wrong self.logger.warn("Unhandled bad relationship: %r", e) raise CerebrumError('The membership was not allowed due to' ' constraints') role.write_db() return "Policy %s is now member of role %s" % (member.component_name, role.component_name) # # policy remove_member # all_commands['policy_remove_member'] = Command( ('policy', 'remove_member'), RoleId(), PolicyId(), perm_filter='is_dns_superuser') def policy_remove_member(self, operator, role_id, member_id): """Try to remove a given member from a given role.""" self.ba.assert_dns_superuser(operator.get_entity_id()) role = self._get_role(role_id) member = self._get_component(member_id) # check if relationship do exists: rel = role.search_relations( source_id=role.entity_id, target_id=member.entity_id, relationship_code=self.const.hostpolicy_contains) if not tuple(rel): raise CerebrumError('%s is not a member of %s' % (member.component_name, role.component_name)) role.remove_relationship(self.const.hostpolicy_contains, member.entity_id) role.write_db() return "Policy %s no longer member of %s" % (member.component_name, role.component_name) # # host list_members # all_commands['policy_list_members'] = Command( ('policy', 'list_members'), RoleId(), fs=FormatSuggestion('%s %s', ('mem_type', 'mem_name'), hdr='Name'), perm_filter='is_dns_superuser') def policy_list_members(self, operator, role_id): """List out all members of a given role.""" self.ba.assert_dns_superuser(operator.get_entity_id()) role = self._get_role(role_id) def _get_members(roleid, increment=0): """Get all direct and indirect members of a given role and return them as list of strings. The hierarchy is presented by a space increment in the strings, e.g. when listing the role "server": database-server postgres-server test-server web-server production-server vortex-server caching-server apache-server test-server """ co = self.const # TODO: there's probably a quicker solution to left padding: inc = ' ' * increment members = role.search_relations( roleid, relationship_code=co.hostpolicy_contains) ret = [] for row in sorted(members, key=lambda r: r['target_name']): type = 'A' if row['target_entity_type'] == co.entity_hostpolicy_role: type = 'R' ret.append({ 'mem_name': row['target_name'], 'mem_type': '%s%s' % (inc, type), }) if row['target_entity_type'] == co.entity_hostpolicy_role: ret.extend(_get_members(row['target_id'], increment + 2)) return ret return _get_members(role.entity_id) # # host policy_add # all_commands['host_policy_add'] = Command(('host', 'policy_add'), HostId(), PolicyId(), perm_filter='is_dns_superuser') def host_policy_add(self, operator, dns_owner_id, comp_id): """Give a host - dns owner - a policy, i.e. a role/atom.""" self.ba.assert_dns_superuser(operator.get_entity_id()) host = self._get_host(dns_owner_id) policy = self._get_component(comp_id) # Do not allow atoms directly on hosts if policy.entity_type == self.const.entity_hostpolicy_atom: raise CerebrumError('Atoms can not be assigned directly to hosts') # check if host already has the policy as direct relation for row in policy.search_hostpolicies(policy_id=policy.entity_id, dns_owner_id=host.entity_id): raise CerebrumError('Host %s already has policy %s' % (host.name, policy.component_name)) # Check if host already has the policy indirectly. Not sure if this # should be a part of the API, as it's not directly an error, but more # of a way of holding the structure somewhat tidy. Note that one could # add a role which have sub roles that is already given to the host, # without getting an error for that. # TODO: this could be swapped with setting indirect_relations to True? def check_member_loop(role_id, check_id): """Find a given check_id in the members of a role, and then raise a CerebrumError with an explanation for this. Works recursively.""" co = self.const for row in policy.search_relations( source_id=role_id, relationship_code=co.hostpolicy_contains): if row['target_id'] == check_id: raise CerebrumError( '%s is a member of the role %s ' '(direct or indirect) - host already ' ' has the role' % (row['target_name'], row['source_name'])) if row['target_entity_type'] == co.entity_hostpolicy_role: check_member_loop(row['target_id'], check_id) if policy.entity_type == self.const.entity_hostpolicy_role: for row in policy.search_hostpolicies(dns_owner_id=host.entity_id): check_member_loop(row['policy_id'], policy.entity_id) # TODO: mutex should be checked here policy.add_to_host(host.entity_id) return "Policy %s added to host %s" % (policy.component_name, host.name) # # host policy_remove # all_commands['host_policy_remove'] = Command( ('host', 'policy_remove'), HostId(), PolicyId(), perm_filter='is_dns_superuser') def host_policy_remove(self, operator, dns_owner_id, comp_id): """Remove a given policy from a given host.""" self.ba.assert_dns_superuser(operator.get_entity_id()) host = self._get_host(dns_owner_id) policy = self._get_component(comp_id) # check that the policy is actually given to the host: if not tuple( policy.search_hostpolicies(policy_id=policy.entity_id, dns_owner_id=host.entity_id)): raise CerebrumError("Host %s doesn't have policy %s" % (host.name, policy.component_name)) policy.remove_from_host(host.entity_id) return "Policy %s removed from host %s" % (policy.component_name, host.name) # # host policy_list # all_commands['host_policy_list'] = Command( ('host', 'policy_list'), HostId(), fs=FormatSuggestion('%-20s %-40s', ('policy_name', 'desc'), hdr='%-20s %-40s' % ('Policy:', 'Description:')), perm_filter='is_dns_superuser') def host_policy_list(self, operator, dns_owner_id): """List all roles/atoms associated to a given host.""" self.ba.assert_dns_superuser(operator.get_entity_id()) host = self._get_host(dns_owner_id) policy = PolicyComponent(self.db) ret = [] for row in policy.search_hostpolicies(dns_owner_id=host.entity_id): policy.clear() policy.find(row['policy_id']) ret.append({ 'policy_name': row['policy_name'], 'desc': policy.description, }) return sorted(ret, key=lambda r: r['policy_name']) # # policy list_hosts # all_commands['policy_list_hosts'] = Command( ('policy', 'list_hosts'), PolicyId(), fs=FormatSuggestion('%s', ('host_or_policy', )), perm_filter='is_dns_superuser') def policy_list_hosts(self, operator, component_id): """List all hosts that has a given policy (role/atom).""" self.ba.assert_dns_superuser(operator.get_entity_id()) comp = self._get_component(component_id) def _get_hosts(policyid, increment=0, already_hosts=[], already_policies=[]): """Recursive function for getting all hosts at the given policy and its parent policies. Returned as a list of strings, where each recursion pads its strings with spaces. Note that both policies and hosts are returned, as one needs to see what subpolicy a host is targeted through. jbofh> polic list_hosts server master-server.uio.no. usit-master.uio.no. crond_running usit-worker-server.uio.no. sharedhost login.uio.no. login.ifi.uio.no. selinux-sharedhosts usit-login.uio.no. logon-test.uio.no. usertestservers test-login.uio.no. abc-server abc-test-server abc-test-server-external abcexttests.uio.no. One can differ between hosts and policies in that all hosts are returned by their FQDN. TBD: Now hosts are only returned through the first found relation, even though it can be related to a policy in many, many ways. Not decided if all relations should be shown for a host. The L{already_policies} contains the policies already listed, to avoid listing the same policy twice. The L{already_hosts} contains hosts already listed, to avoid listing the same host twice. """ # TODO: there's probably a quicker solution to left padding: inc = ' ' * increment # get this policy's hosts ret = [] for row in comp.search_hostpolicies(policy_id=policyid): h_id = row['dns_owner_id'] if h_id in already_hosts: continue already_hosts.append(h_id) ret.append({ 'host_or_policy': '%s%s' % (inc, row['dns_owner_name']), }) # get parent policies if they have hosts related to them parent = comp.search_relations( target_id=policyid, relationship_code=self.const.hostpolicy_contains) for row in sorted(parent, key=lambda r: r['source_name']): if row['source_id'] in already_policies: continue already_policies.append(row['source_id']) subs = _get_hosts(row['source_id'], increment + 2, already_hosts, already_policies) if subs: ret.append({ 'host_or_policy': '%s%s' % (inc, row['source_name']), }) ret.extend(subs) return sorted(ret) return sorted(_get_hosts(comp.entity_id)) # # policy has_member # all_commands['policy_has_member'] = Command( ('policy', 'has_member'), PolicyId(), fs=FormatSuggestion('%-20s', ('policy_name', ), hdr='%-20s' % ('Policy', )), perm_filter='is_dns_superuser') def policy_has_member(self, operator, component_id): """List all hosts and/or roles that is related to the given component.""" self.ba.assert_dns_superuser(operator.get_entity_id()) comp = self._get_component(component_id) def _get_parents(policyid, increment=0, already_processed=[]): """Get all direct and indirect parents of a given policy and return them as list of strings. The hierarchy is presented by a space increment in the strings, e.g. when listing the policy "abc_test_server": test_servers server machine abc_environment usit_env unix_any We could have used search_relations with indirect_relations=True, but then we couldn't see the hierarchy. The L{already_processed} contains the policies already listed, to avoid listing policies twice. """ # TODO: there's probably a quicker solution to left padding: inc = ' ' * increment parents = comp.search_relations( target_id=policyid, relationship_code=self.const.hostpolicy_contains) ret = [] for row in sorted(parents, key=lambda r: r['source_name']): ret.append({ 'policy_name': '%s%s' % (inc, row['source_name']), }) if row['source_id'] not in already_processed: ret.extend( _get_parents(row['source_id'], increment + 2, already_processed)) already_processed.append(row['source_id']) return ret return _get_parents(comp.entity_id, 0, []) # # policy list_atoms # all_commands['policy_list_atoms'] = Command( ('policy', 'list_atoms'), Filter(), fs=FormatSuggestion('%-20s %-30s', ('name', 'desc'), hdr='%-20s %-30s' % ('Name', 'Description'))) def policy_list_atoms(self, operator, filter): """Return a list of atoms that match the given filters.""" # This method is available for everyone atom = Atom(self.db) filters = self._parse_filters(filter, { 'name': None, 'date': self._parse_create_date_range, 'create': self._parse_create_date_range, 'desc': None, 'foundation': None }, default_filter='name', default_value=None) date_start = date_end = None if filters['date']: date_start, date_end = filters['date'] if date_end is NotSet: # only the specific date should be used date_end = date_start create_start = create_end = None if filters['create']: create_start, create_end = filters['create'] if create_end is NotSet: # only the specific date should be used create_end = create_start ret = [] for row in atom.search(name=filters['name'], description=filters['desc'], create_start=create_start, create_end=create_end, foundation_start=date_start, foundation_end=date_end, foundation=filters['foundation']): ret.append({ 'name': row['name'], 'desc': row['description'], }) return sorted(ret, key=lambda r: r['name']) # # policy list_roles # all_commands['policy_list_roles'] = Command( ('policy', 'list_roles'), Filter(), fs=FormatSuggestion('%-20s %-30s', ('name', 'desc'), hdr='%-20s %-30s' % ('Name', 'Description'))) def policy_list_roles(self, operator, filter): """Return a list of roles that match the given filters.""" # This method is available for everyone role = Role(self.db) filters = self._parse_filters(filter, { 'name': str, 'date': self._parse_create_date_range, 'create': self._parse_create_date_range, 'desc': str, 'foundation': str }, default_filter='name', default_value=None) date_start = date_end = None if filters['date']: date_start, date_end = filters['date'] if date_end is NotSet: # only the specific date should be used date_end = date_start create_start = create_end = None if filters['create']: create_start, create_end = filters['create'] if create_end is NotSet: # only the specific date should be used create_end = create_start ret = [] for row in role.search(name=filters['name'], description=filters['desc'], create_start=create_start, create_end=create_end, foundation_start=date_start, foundation_end=date_end, foundation=filters['foundation']): ret.append({ 'name': row['name'], 'desc': row['description'], }) return sorted(ret, key=lambda r: r['name']) # # policy info # all_commands['policy_info'] = Command( ('policy', 'info'), RoleId(), fs=FormatSuggestion([ ('Name: %-30s\n' 'Created: %-30s\n' 'Description: %-30s\n' 'Foundation: %-30s\n' 'Foundation date: %-30s\n' 'Type: %-30s', ('name', format_day('create_date'), 'desc', 'foundation', format_day('foundation_date'), 'type')), ('Relation: %s (%s)', ('target_rel_name', 'target_rel_type'), ('Direct relationships where this role is target:')), ('Relation: %s (%s)', ('rel_name', 'rel_type'), ('Direct relationships where this role is source:')), ])) def policy_info(self, operator, policy_id): """Return information about a policy component.""" # This method is available for everyone comp = self._get_component(policy_id) ret = [{ 'name': comp.component_name, 'type': text_type(self.const.EntityType(comp.entity_type)), 'create_date': comp.created_at, 'desc': comp.description, 'foundation': comp.foundation, 'foundation_date': comp.foundation_date, }] # check what this component is in relationship with for row in comp.search_relations(target_id=comp.entity_id): ret.append({ 'target_rel_name': row['source_name'], 'target_rel_type': row['relationship_str'], }) # if this is a role, add direct relationships where this is the source if comp.entity_type == self.const.entity_hostpolicy_role: for row in comp.search_relations(source_id=comp.entity_id): ret.append({ 'rel_name': row['target_name'], 'rel_type': row['relationship_str'], }) return ret
class BofhdVoipCommands(BofhdCommonMethods): """Bofhd extension with voip commands.""" all_commands = dict() parent_commands = False authz = bofhd_voip_auth.BofhdVoipAuth @classmethod def get_help_strings(cls): return (bofhd_voip_help.group_help, bofhd_voip_help.command_help, bofhd_voip_help.arg_help) ######################################################################## # supporting methods # def _human_repr2id(self, human_repr): """Just like the superclass, except mac addresses are trapped here.""" if (isinstance(human_repr, (str, unicode)) and len(human_repr) == 17 and all(x.lower() in "0123456789abcdef: " for x in human_repr)): return "mac", human_repr return super(BofhdVoipCommands, self)._human_repr2id(human_repr) def _get_entity_id(self, designation): """Check whetner L{designation} may be interpreted as an entity id. Return the id, if possible, or None, when no conversion applies. We consider the following as entity_id: + sequence of digits + string with sequence of digits + string of the form 'id:digits' """ id_type, value = self._human_repr2id(designation) if id_type == "id": return int(value) return None def _is_numeric_id(self, value): return (isinstance(value, (int, long)) or isinstance(value, (str, unicode)) and value.isdigit()) def _get_constant(self, designation, const_type): """Fetch a single constant based on some human designation. Return the first match given by 'designation' (see Constants.py:fetch_constants). FIXME: refactor this to bofhd_core.py ? """ cnst = self.const.fetch_constants(const_type, designation) if len(cnst) == 0: raise CerebrumError("Unknown %s constant: %s" % (str(const_type), designation)) return cnst[0] def _get_ou(self, designation): """Fetch a single OU identified by designation. TODO: extend this method to be generous about what's accepted as input. TODO: how do we decide if 123456 means sko or entity_id? FIXME: compare this with other incarnations of _get_ou. Maybe there can be ONE method that covers all the cases with an obvious interface? E.g. we should accept things like 33-15-20 as it is OBVIOUSLY a sko. """ id_type, value = self._human_repr2id(designation) if str(value).isdigit(): for key in ("ou_id", "stedkode"): try: x = super(BofhdVoipCommands, self)._get_ou(**{key: str(value)}) return x except (Errors.NotFoundError, CerebrumError): pass raise CerebrumError("Could not find ou by designation %s" % str(designation)) def _get_voip_service(self, designation): """Locate a specific voip_service. We try to be a bit lax when it comes to identifying voip_services. A numeric designation is interpreted as entity_id. Everything else is interpreted as description. """ service = VoipService(self.db) if self._get_entity_id(designation): try: service.find(self._get_entity_id(designation)) return service except Errors.NotFoundError: raise CerebrumError("Could not find voip service with id=%s" % str(designation)) ids = service.search_voip_service_by_description(designation, exact_match=True) if len(ids) == 1: service.find(ids[0]["entity_id"]) return service raise CerebrumError("Could not uniquely determine voip_service " "from description %s" % str(designation)) def _get_voip_address_by_service_description(self, designation): try: service = self._get_voip_service(designation) return self._get_voip_address_by_owner_entity_id(service.entity_id) except (CerebrumError, Errors.NotFoundError): return list() def _get_voip_address_by_owner_account(self, designation): try: account = self._get_account(designation) return self._get_voip_address_by_owner_entity_id(account.owner_id) except (CerebrumError, Errors.NotFoundError): return list() def _get_voip_address_by_contact_info(self, designation): try: ids = set(x["entity_id"] for x in self._get_contact_info(designation)) result = list() for x in ids: result.extend(self._get_voip_address_by_owner_entity_id(x)) return result except Errors.NotFoundError: return list() assert False, "NOTREACHED" def _get_voip_address_by_owner_entity_id(self, designation): if not self._is_numeric_id(designation): return list() value = int(designation) try: va = VoipAddress(self.db) va.find_by_owner_id(value) return [ va, ] except Errors.NotFoundError: return list() assert False, "NOTREACHED" def _get_voip_address_by_entity_id(self, designation): """Return all voip_addresses matching the specified entity_id.""" if not self._is_numeric_id(designation): return list() value = int(designation) try: va = VoipAddress(self.db) va.find(value) return [ va, ] except Errors.NotFoundError: return list() assert False, "NOTREACHED" def _get_voip_address(self, designation, all_matches=False): """Collect voipAddress instance(s) matching designation. Crap. This turned out to be extremely complicated, since there are so many ways we could interpret the meaning of 'give a voip_address for <doohickey>'. @param all_matches: Controls whether to collect all possible matches, or just the first one. Note that the default case is to collect the single voipAddress that matches the first of the search criteria below. Even though previous searches may have matched multiple addresses, the first search yielding exactly one answer will be used. """ search_names = ( "by_entity_id", "by_owner_entity_id", "by_contact_info", "by_owner_account", "by_service_description", ) id_type, value = self._human_repr2id(designation) result = list() collected_ids = set() for partial_name in search_names: caller_name = "_get_voip_address_" + partial_name caller = getattr(self, caller_name) addrs = caller(value) # self.logger.debug("Searcher %s returned %d VA(s)", caller_name, # len(addrs)) result.extend(x for x in addrs if x.entity_id not in collected_ids) collected_ids.update(x.entity_id for x in addrs) # If an exact match is requested, grab the first search that yields # exactly 1 answer. if not all_matches and len(addrs) == 1: return addrs[0] if len(result) == 0: raise CerebrumError("No voip_address matches designation %s" % (designation, )) if not all_matches and len(result) > 1: raise CerebrumError( "Cannot uniquely determine voip_address from " "designation %s: matching ids=%s" % (designation, ", ".join(str(x.entity_id) for x in result))) return result def _get_or_create_voip_address(self, owner_id, with_softphone=True): """Much like _get_voip_address(), except this one creates it as well if it does not exist, rather than failing. with_softphone controls whether we want to create a softphone for the new voip_address, should voip_address be created. """ address = VoipAddress(self.db) try: address.find_by_owner_id(owner_id) except Errors.NotFoundError: address.clear() address.populate(owner_id) address.write_db() address.write_db() if with_softphone: self._create_default_softphone_client(address.entity_id) return address def _create_default_softphone_client(self, voip_address_id): """Help function to create default softphone client for all addresses and services. """ # # If it exists, we are done... client = VoipClient(self.db) if client.search(voip_address_id=voip_address_id, client_type=self.const.voip_client_type_softphone): return client.populate( voip_address_id, self.const.voip_client_type_softphone, True, # sip_enabled by default None, # softphones don't have MACs self.const.voip_client_info_softphone) client.write_db() client.set_auth_data(self.const.voip_auth_sip_secret, client.generate_sip_secret()) self.logger.debug( "Automatically generated softphone " "client id=%s for address %s", client.entity_id, voip_address_id) def _get_voip_client(self, designation): """Locate a voip_client by designation. Possible interpretations of designation are: + entity_id + mac address """ # Don't use _human_repr2id here, since it does not like ':' being part # of the identifier (which mac addresses definitely have) client = VoipClient(self.db) if (isinstance(designation, (int, long)) or isinstance(designation, str) and designation.isdigit()): try: client.find(int(designation)) return client except Errors.NotFoundError: pass # Try to look up by mac address try: client.clear() client.find_by_mac_address(designation) return client except (Errors.NotFoundError, AssertionError): pass raise CerebrumError("Could not uniquely determine voip_client " "from designation %s" % str(designation)) def _get_voip_person(self, designation): """Lookup a person by <something>. """ person = Factory.get("Person")(self.db) if self._get_entity_id(designation): try: person.find(self._get_entity_id(designation)) return person except Errors.NotFoundError: pass # fnr? exc = CerebrumError("No person found for designation %s" % str(designation)) id_type, value = self._human_repr2id(designation) if str(value).isdigit(): try: fnr = personnr_ok(str(value)) person.find_by_external_id(self.const.externalid_fodselsnr, fnr) return person except (InvalidFnrError, Errors.NotFoundError): pass # account? try: account = Factory.get("Account")(self.db) account.find_by_name(str(value)) if account.owner_type == self.const.entity_person: return self._get_voip_person(account.owner_id) except Errors.NotFoundError: pass # By other external ids? By name? raise exc def _get_voip_owner(self, designation): """Locate the owner of a voip address. We try in order to look up voip-service and then person. If the search is unsuccessful, throw CerebrumError. @param designation: Some sort of identification of the owner. @return: A voipService/Person instance, or raise CerebrumError if nothing appropriate is found. """ for method in (self._get_voip_person, self._get_voip_service): try: return method(designation) except CerebrumError: pass raise CerebrumError("Cannot locate person/voip-service designated " "by %s" % str(designation)) def _get_contact_info(self, value): """Return a sequence of dicts containing entity_contact_info entries for the specified contact value. We scan system_voip ONLY. value is matched both against contact_value and contact_alias. """ result = list() eci = EntityContactInfo(self.db) for row in eci.list_contact_info(source_system=self.const.system_voip, contact_value=str(value)): result.append(row.dict()) for row in eci.list_contact_info(source_system=self.const.system_voip, contact_alias=str(value)): result.append(row.dict()) return result def _collect_constants(self, const_type): """Return a suitable data structure containing all constants of the given type. """ result = list() for cnst in self.const.fetch_constants(const_type): result.append({ "code": int(cnst), "code_str": str(cnst), "description": cnst.description }) return sorted(result, key=lambda r: r["code_str"]) def _typeset_traits(self, trait_sequence): """Return a human-friendly version of entity traits. """ traits = list() for et in trait_sequence: traits.append("%s" % (str(self.const.EntityTrait(et)), )) return traits def _typeset_ou(self, ou_id): """Return a human-friendly description of an OU. Return something like '33-15-20 USIT' """ ou = self._get_ou(int(ou_id)) # Use acronym, if available, otherwise use short-name acronym = ou.get_name_with_language(self.const.ou_name_acronym, self.const.language_nb, None) short_name = ou.get_name_with_language(self.const.ou_name_short, self.const.language_nb, None) ou_name = acronym and acronym or short_name location = "%02d-%02d-%02d (%s)" % (ou.fakultet, ou.institutt, ou.avdeling, ou_name) return location def _typeset_bool(self, value): """Return a human-friendly version of a boolean. """ return bool(value) def _assert_unused_service_description(self, description): """Check that a description is not in use by any voip_service. """ vs = VoipService(self.db) services = vs.search_voip_service_by_description(description, exact_match=True) if services: raise CerebrumError("Description must be unique. In use by id=%s." \ % services[0]['entity_id']) ######################################################################## # voip_service related commands # all_commands["voip_service_new"] = Command(("voip", "service_new"), VoipServiceTypeCode(), OU(), SimpleString()) def voip_service_new(self, operator, service_type, ou_tag, description): """Create a new voip_service. @param service_type: Type of voip_service entry. @param ou_tag: OU where the voip_service located (stedkode) @param description: service's description. Must be globally unique. """ self.ba.can_create_voip_service(operator.get_entity_id()) self._assert_unused_service_description(description) service = VoipService(self.db) ou = self._get_ou(ou_tag) service_type = self._get_constant(service_type, self.const.VoipServiceTypeCode) if service_type is None: raise CerebrumError("Unknown voip_service_type: %s" % str(service_type)) service.populate(description, service_type, ou.entity_id) service.write_db() # Create a corresponding voip_address... self._get_or_create_voip_address(service.entity_id) return "OK, new voip_service (%s), entity_id=%s" % (str(service_type), service.entity_id) # end voip_service_new all_commands["voip_service_info"] = Command(("voip", "service_info"), VoipServiceParameter(), fs=FormatSuggestion( "Entity id: %d\n" "Service type: %s\n" "Description: %s\n" "Location: %s\n" "Traits: %s\n", ( "entity_id", "service_type", "description", "location", "traits", ))) def voip_service_info(self, operator, designation): """Return information about a voip_service. @param designation: either an entity_id or a description. """ self.ba.can_view_voip_service(operator.get_entity_id()) service = self._get_voip_service(designation) answer = { "entity_id": service.entity_id, "service_type": str(self.const.VoipServiceTypeCode(service.service_type)), "description": service.description, } answer["location"] = self._typeset_ou(service.ou_id) answer["traits"] = self._typeset_traits(service.get_traits()) return answer # end voip_service_info all_commands["voip_service_type_list"] = Command( ("voip", "service_type_list"), fs=FormatSuggestion("%-32s %s", ("code_str", "description"), hdr="%-32s %s" % ("Type", "Description"))) def voip_service_type_list(self, operator): """List available service_info_code values.""" return self._collect_constants(self.const.VoipServiceTypeCode) # end voip_service_type_list all_commands["voip_service_delete"] = Command(("voip", "service_delete"), VoipServiceParameter()) def voip_service_delete(self, operator, designation): """Delete the specified voip_service. Caveat: this method assumes that the related voip_address/clients have already been deleted. This is by design. """ self.ba.can_create_voip_service(operator.get_entity_id()) service = self._get_voip_service(designation) entity_id, description = service.entity_id, service.description service.delete() return "OK, deleted voip_service id=%s, %s" % (entity_id, description) # end voip_service_delete all_commands["voip_service_update"] = Command( ("voip", "service_update"), VoipServiceParameter(), SimpleString(optional=True, default=None), VoipServiceTypeCode(optional=True, default=None), OU(optional=True, default=None)) def voip_service_update(self, operator, designation, description=None, service_type=None, ou_tag=None): """Update information about an existing voip_service. Those attributes that are None are left alone. """ self.ba.can_alter_voip_service(operator.get_entity_id()) service = self._get_voip_service(designation) if description and service.description != description: self._assert_unused_service_description(description) service.description = description if service_type: service_type = self._get_constant(service_type, self.const.VoipServiceTypeCode) if service.service_type != service_type: service.service_type = service_type if ou_tag: ou = self._get_ou(ou_tag) if service.ou_id != ou.entity_id: service.ou_id = ou.entity_id service.write_db() return "OK, updated information for voip_service id=%s" % ( service.entity_id, ) # end voip_service_update all_commands["voip_service_find"] = Command( ("voip", "service_find"), SimpleString(), fs=FormatSuggestion("%8i %15s %25s %8s", ("entity_id", "description", "service_type", "ou"), hdr="%8s %15s %25s %8s" % ("EntityId", "Description", "Type", "Stedkode"))) def voip_service_find(self, operator, designation): """List all voip_services matched in some way by designation. This has been requested to ease up looking up voip_services for users. designation is used to look up voip_services in the following fashion: - if all digits -> by entity_id - by description (exactly) - by description (substring search) - if all digits -> by ou_id - if all digits -> by stedkode All the matching voip_services are collected and returned as a sequence so people can pluck out the entities they want and use them in subsequent commands. """ def fold_description(s): cutoff = 15 suffix = "(...)" if len(s) > cutoff: return s[:(cutoff - len(suffix))] + suffix return s # end fold_description self.ba.can_view_voip_service(operator.get_entity_id()) ident = designation.strip() collect = dict() vs = VoipService(self.db) # let's try by-id lookup first if ident.isdigit(): try: vs.find(int(ident)) collect[vs.entity_id] = (vs.description, vs.service_type, vs.ou_id) except Errors.NotFoundError: pass # then by-description... for exact_match in (False, True): results = vs.search_voip_service_by_description( designation, exact_match=exact_match) for row in results: collect[row["entity_id"]] = (row["description"], row["service_type"], row["ou_id"]) # then by OU (stedkode and ou_id) try: ou = self._get_ou(designation) for row in vs.search(ou_id=ou.entity_id): collect[row["entity_id"]] = (row["description"], row["service_type"], row["ou_id"]) except CerebrumError: pass # Finally, the presentation layer if len(collect) > cereconf.BOFHD_MAX_MATCHES: raise CerebrumError("More than %d (%d) matches, please narrow " "search criteria" % (cereconf.BOFHD_MAX_MATCHES, len(collect))) answer = list() for entity_id in collect: description, service_type, ou_id = collect[entity_id] answer.append({ "entity_id": entity_id, "description": fold_description(description), "service_type": str(self.const.VoipServiceTypeCode(service_type)), "ou": self._typeset_ou(ou_id), }) return answer # end voip_service_find ######################################################################## # voip_client related commands # # voip TODO # all_commands["voip_client_new"] = Command( ("voip", "client_new"), VoipOwnerParameter(), VoipClientTypeCode(), MacAddress(), VoipClientInfoCode(), YesNo(help_ref='yes_no_sip_enabled', default="Yes")) def voip_client_new(self, operator, owner_designation, client_type, mac_address, client_info, sip_enabled=True): """Create a new voip_client. If the owner (be it voip_service or person) does NOT have a voip_address, create that as well. """ self.ba.can_create_voip_client(operator.get_entity_id()) # Find the owner first... owner = self._get_voip_owner(owner_designation) if isinstance(sip_enabled, (str, unicode)): sip_enabled = self._get_boolean(sip_enabled) # Does that mac_address point to something? try: self._get_voip_client(mac_address) raise CerebrumError("Mac address %s is already bound to a " "voip_client." % str(mac_address)) except CerebrumError: pass # Check that info/type_code make sense... ct = self._get_constant(client_type, self.const.VoipClientTypeCode) ci = self._get_constant(client_info, self.const.VoipClientInfoCode) if not ((ct == self.const.voip_client_type_softphone and not mac_address) or (ct == self.const.voip_client_type_hardphone and mac_address)): raise CerebrumError("Hardphones must have mac; softphones must " "not: %s -> %s" % (str(ct), mac_address)) # get/create an address for that owner already... address = self._get_or_create_voip_address( owner.entity_id, with_softphone=(ct == self.const.voip_client_type_softphone)) client = VoipClient(self.db) client.populate(address.entity_id, ct, sip_enabled, mac_address, ci) client.write_db() client.set_auth_data(self.const.voip_auth_sip_secret, client.generate_sip_secret()) return "OK, created voipClient %s, id=%s" % (str(ct), client.entity_id) # # voip TODO # all_commands["voip_client_info"] = Command( ("voip", "client_info"), VoipClientParameter(), fs=FormatSuggestion( "Entity id: %d\n" "Client type: %s\n" "Client info: %s\n" "Mac address: %s\n" "sip enabled: %s\n" "has secret?: %s\n" "Traits: %s\n" "voipAddress id: %d\n" "voipAddress' owner: %s\n", ("entity_id", "client_type", "client_info", "mac_address", "sip_enabled", "has_secret", "traits", "voip_address_id", "owner"))) def voip_client_info(self, operator, designation): """Return information about a voip_client. """ self.ba.can_view_voip_client(operator.get_entity_id()) client = self._get_voip_client(designation) address = self._get_voip_address(client.voip_address_id) # This is such a bloody overkill... We just need id/name But f**k it, # CPU cycles are cheap. address_attrs = address.get_voip_attributes() client_attrs = client.get_voip_attributes() owner = "cn=%s (id=%s)" % (address_attrs["cn"], address.owner_entity_id) answer = { "entity_id": client.entity_id, "client_type": client_attrs["sipClientType"], "client_info": client_attrs["sipClientInfo"], "mac_address": client_attrs["sipMacAddress"], "sip_enabled": self._typeset_bool(client_attrs["sipEnabled"]), "has_secret": bool(client.get_auth_data(self.const.voip_auth_sip_secret)), "traits": self._typeset_traits(client.get_traits()), "voip_address_id": client.voip_address_id, "owner": owner, } return answer # # voip TODO # all_commands["voip_client_list_info_code"] = Command( ("voip", "client_list_info_code"), fs=FormatSuggestion("%-25s %s", ("code_str", "description"), hdr="%-25s %s" % ("Client info code", "Description"))) def voip_client_list_info_code(self, operator): """List all possible voip_client info codes.""" return self._collect_constants(self.const.VoipClientInfoCode) # # voip TODO # all_commands["voip_client_list_type_code"] = Command( ("voip", "client_list_type_code"), fs=FormatSuggestion("%-25s %s", ("code_str", "description"), hdr="%-25s %s" % ("Client type code", "Description"))) def voip_client_list_type_code(self, operator): """List all possible voip_client type codes. """ return self._collect_constants(self.const.VoipClientTypeCode) # # voip TODO # all_commands["voip_client_delete"] = Command(("voip", "client_delete"), VoipClientParameter()) def voip_client_delete(self, operator, designation): """Remove (completely) a voip_client from Cerebrum.""" self.ba.can_create_voip_client(operator.get_entity_id()) client = self._get_voip_client(designation) entity_id, mac = client.entity_id, client.mac_address client.delete() return "OK, removed voipClient id=%s (mac=%s)" % (entity_id, mac) # # voip TODO # all_commands["voip_client_set_info_code"] = Command( ("voip", "client_set_info_code"), VoipClientParameter(), VoipClientInfoCode()) def voip_client_set_info_code(self, operator, designation, new_info): """Change client_info for a specified client.""" self.ba.can_alter_voip_client(operator.get_entity_id()) ci = self._get_constant(new_info, self.const.VoipClientInfoCode) client = self._get_voip_client(designation) if client.client_type != self.const.voip_client_type_hardphone: raise CerebrumError("Can only change hardphones.") client.client_info = ci client.write_db() return "OK, changed for voipClient id=%s" % client.entity_id # # voip client_sip_enabled <client> yes|no # all_commands["voip_client_sip_enabled"] = Command( ("voip", "client_sip_enabled"), VoipClientParameter(), YesNo(help_ref='yes_no_sip_enabled')) def voip_client_sip_enabled(self, operator, designation, yesno): """Set sip_enabled to True/False for a specified client.""" self.ba.can_alter_voip_client(operator.get_entity_id()) client = self._get_voip_client(designation) status = self._get_boolean(yesno) if client.sip_enabled == status: return "OK (no changes for client id=%s)" % (client.entity_id) client.sip_enabled = status client.write_db() return "OK (changed sip_enabled to %s for client id=%s)" % ( client.sip_enabled, client.entity_id) # # voip client_secrets_reset <client> # all_commands["voip_client_secrets_reset"] = Command( ("voip", "client_secrets_reset"), VoipClientParameter()) def voip_client_secrets_reset(self, operator, designation): """Reset all of voip_client's secrets. This is useful if a client has been compromised and needs to be reset. """ client = self._get_voip_client(designation) self.ba.can_reset_client_secrets(operator.get_entity_id(), client.entity_id) for secret_kind in (self.const.voip_auth_sip_secret, ): secret = client.generate_sip_secret() client.set_auth_data(secret_kind, secret) return "OK (reset sip secrets for voip_client id=%s)" % ( client.entity_id, ) # end voip_client_secrets_reset # # voip client_new_secret <client> <secret> # all_commands["voip_client_new_secret"] = Command( ("voip", "client_new_secret"), VoipClientParameter(), SimpleString()) def voip_client_new_secret(self, operator, designation, new_secret): """Register a new sipSecret for the specified client.""" client = self._get_voip_client(designation) self.ba.can_set_new_secret(operator.get_entity_id(), client.entity_id) # First check the new_secret quality. client.validate_auth_data(self.const.voip_auth_sip_secret, new_secret) # Locate current secret current_secret = client.get_auth_data(self.const.voip_auth_sip_secret) # Register new_secret client.set_auth_data(self.const.voip_auth_sip_secret, new_secret) return "OK (set new sip secret for voip_client id=%s)" % ( client.entity_id, ) ######################################################################## # voip_address related commands # # voip address_list_contact_codes # all_commands["voip_address_list_contact_codes"] = Command( ("voip", "address_list_contact_codes"), fs=FormatSuggestion("%-25s %s", ("code_str", "description"), hdr="%-25s %s" % ("Code", "Description"))) def voip_address_list_contact_codes(self, operator): """List all available contact_info_codes. """ return self._collect_constants(self.const.ContactInfo) # # voip address_add_number <owner> <ctype> <TODO> [TODO] # all_commands["voip_address_add_number"] = Command( ("voip", "address_add_number"), VoipOwnerParameter(), ContactTypeParameter(), SimpleString(help_ref="voip_extension_full"), PriorityParameter(optional=True, default=None)) def voip_address_add_number(self, operator, designation, contact_type, contact_full, priority=None): """Add a new phone number to a voip_address at given priority. The number is added, assuming that no voip_address already owns either full or internal number. internal_number must be set. If no priority is specified, one is chosen automatically (it would be the lowest possible priority) """ def owner_has_contact(owner, contact_value): entity_ids = [ x["entity_id"] for x in self._get_contact_info(contact_value) ] return owner.entity_id in entity_ids # end owner_has_contact def next_priority(owner, priority=None): if priority is None: so_far = owner.list_contact_info( entity_id=owner.entity_id, source_system=self.const.system_voip) result = 1 if so_far: result = max(x["contact_pref"] for x in so_far) + 1 return result priority = int(priority) if priority <= 0: raise CerebrumError("Priority must be larger than 0") return priority # end next_priority self.ba.can_alter_number(operator.get_entity_id()) # Make sure that the number is not in use. if self._get_contact_info(contact_full): raise CerebrumError("Number %s already in use." % contact_full) # Is there an address for that owner already? owner = self._get_voip_owner(designation) address = self._get_or_create_voip_address(owner.entity_id) if contact_type: contact_type = self._get_constant(contact_type, self.const.ContactInfo) else: contact_type = self.const.contact_voip_extension # Deduce 5-digit alias, if contact info is of the proper type. contact_alias = None if contact_type == self.const.contact_voip_extension: contact_alias = contact_full[-5:] # Check the numbers for syntax errors if not address.contact_is_valid(contact_type, contact_full, contact_alias): raise CerebrumError("(%s, %s) number pair is not a valid number." % (contact_full, contact_alias)) # Have these numbers already been registered to owner? if (owner_has_contact(owner, contact_full) or owner_has_contact(owner, contact_alias)): raise CerebrumError("%s or %s has already been registered " "for %s." % (contact_full, contact_alias, owner.entity_id)) # Figure out the priority if it has not been specified priority = next_priority(owner, priority) owner.add_contact_info(self.const.system_voip, contact_type, contact_full, pref=priority, alias=contact_alias) warn = "." if contact_alias and not contact_full.endswith(contact_alias): warn = " (Short number does not match full number)." return "OK, associated %s/%s with owner id=%s%s" % ( contact_full, contact_alias, owner.entity_id, warn) # # voip address_delete_number <TODO> [TODO] # all_commands["voip_address_delete_number"] = Command( ("voip", "address_delete_number"), SimpleString(help_ref="voip_extension_full"), VoipOwnerParameter(optional=True, default=None)) def voip_address_delete_number(self, operator, value, owner=None): """Delete a previously registered phone number. If a phone number is associated with several entities, we cowardly refuse, unless owner is specified. @param value: Contact info string. Could be full or short version of the phone number. """ self.ba.can_alter_number(operator.get_entity_id()) contacts = self._get_contact_info(value) if not contacts: return "OK, nothing to delete (no record of %s)" % (str(value)) if owner is not None: owner = self._get_voip_owner(owner) if len(contacts) > 1 and owner is None: raise CerebrumError("Multiple entities have %s registered. You " "must specified entity to update." % (value, )) eci = EntityContactInfo(self.db) victims = set() for d in contacts: if owner and owner.entity_id != d["entity_id"]: continue eci.clear() eci.find(d["entity_id"]) eci.delete_contact_info(d["source_system"], d["contact_type"], d["contact_pref"]) victims.add(eci.entity_id) return "OK, removed %s from %d entit%s: %s" % ( str(value), len(victims), len(victims) != 1 and "ies" or "y", ", ".join("id=%s" % x for x in victims)) # # voip address_info <voip-addr> # all_commands["voip_address_info"] = Command( ("voip", "address_info"), VoipAddressParameter(), fs=FormatSuggestion( "Entity id: %d\n" "Owner entity id: %d\n" "Owner type: %s\n" "cn: %s\n" "sipURI: %s\n" "sipPrimaryURI: %s\n" "e164URI: %s\n" "Extension URI: %s\n" "Traits: %s\n" "Clients: %s\n", ( "entity_id", "owner_entity_id", "owner_entity_type", "cn", "sip_uri", "sip_primary_uri", "e164_uri", "extension_uri", "traits", "clients", ))) def voip_address_info(self, operator, designation): """Display information about ... ? The spec says 'all attributes': uname, cn, all URIs, e-mail, etc. @param designation: uid, id, fnr -- some way of identifying the proper voip-address. FIXME: Should be split designation into key:value, where key is one of (uname, cn, phone, entity_id/id) and value is the string interpreted according to the meaning of the first key. """ address = self._get_voip_address(designation) owner = address.get_owner() # find the clients client = VoipClient(self.db) client_ids = sorted([ str(x["entity_id"]) for x in client.search(voip_address_id=address.entity_id) ]) attrs = address.get_voip_attributes() result = { "entity_id": address.entity_id, "owner_entity_id": owner.entity_id, "owner_entity_type": str(self.const.EntityType(owner.entity_type)), "cn": attrs["cn"], "sip_uri": attrs["voipSipUri"], "sip_primary_uri": attrs["voipSipPrimaryUri"], "e164_uri": attrs["voipE164Uri"], "extension_uri": attrs["voipExtensionUri"], "traits": self._typeset_traits(address.get_traits()), "clients": client_ids, } return result # # void address_delete <voip-addr> # all_commands["voip_address_delete"] = Command(("voip", "address_delete"), VoipAddressParameter()) def voip_address_delete(self, operator, designation): """Delete a voip_address from Cerebrum. This is useful as a precursor to removing a service. """ self.ba.can_alter_voip_address(operator.get_entity_id()) address = self._get_voip_address(designation) # Without this check users risk seeing an internal error, rather than # a detailed error message. client = VoipClient(self.db) clients = list(client.search(voip_address_id=address.entity_id)) if clients: raise CerebrumError("Won't delete address id=%s: " "it has %d voip_client(s)" % (address.entity_id, len(clients))) address_id = address.entity_id owner = address.get_owner() address.delete() return "OK, deleted voip_address id=%s (owned by %s id=%s)" % ( address_id, self.const.EntityType( owner.entity_type), owner.entity_id) # # voip address_find <search-param> # all_commands["voip_address_find"] = Command( ("voip", "address_find"), SimpleString(), fs=FormatSuggestion( "%9i %14i %13s %25s", ("entity_id", "owner_entity_id", "owner_type", "cn"), hdr="%9s %14s %13s %25s" % ("EntityId", "OwnerEntityId", "Owner type", "CN"))) def voip_address_find(self, operator, designation): """List all voip_addresses matched in some way by designation. This has been requested to ease up searching for specific voip_addresses. """ vas = self._get_voip_address(designation, all_matches=True) # Finally, the presentation layer if len(vas) > cereconf.BOFHD_MAX_MATCHES: raise CerebrumError("More than %d (%d) matches, please narrow down" "the search criteria" % (cereconf.BOFHD_MAX_MATCHES, len(vas))) answer = list() for va in vas: # NB! This call is pretty expensive. We may want to revise # BOFHD_MAX_MATCHES-cutoff here (and rather replace it with # something more stringent) voip_attrs = va.get_voip_attributes() answer.append({ "entity_id": va.entity_id, "owner_entity_id": va.owner_entity_id, "owner_type": str(voip_attrs["owner_type"]), "cn": voip_attrs["cn"], }) return answer
for row in self._get_guests(): try: ret.append(self._get_guest_info(row['entity_id'])) except CerebrumError, e: print "Error: %s" % e continue if not ret: raise CerebrumError("Found no guest accounts.") return ret hidden_commands['guest_reset_password'] = Command( ('guest', 'reset_password'), AccountName(), fs=FormatSuggestion([ ('New password for user %s, notified %s by SMS.', ( 'username', 'mobile', )), ]), perm_filter='can_reset_guest_password') def guest_reset_password(self, operator, username): """ Reset the password of a guest account. :param BofhdSession operator: The operator :param string username: The username of the guest account :return dict: A dictionary with keys 'username' and 'mobile' """ account = self._get_account(username) self.ba.can_reset_guest_password(operator.get_entity_id(),
class BofhdHistoryCmds(BofhdCommandBase): """BofhdExtension for history related commands and functionality.""" all_commands = {} authz = BofhdHistoryAuth @property def util(self): try: return self.__util except AttributeError: self.__util = BofhdUtils(self.db) return self.__util @classmethod def get_help_strings(cls): """Get help strings.""" group_help = { 'history': "History related commands", } command_help = { 'history': { 'history_show': 'List changes made to an entity', } } argument_help = { 'limit_number_of_results': [ 'limit', 'Number of changes to list', 'Upper limit for how many changes to include, counting ' 'backwards from the most recent. Default (when left empty) ' 'is 0, which means no limit' ], 'yes_no_all_changes': [ 'all', 'All involved changes?', 'List all changes where the entity is involved (yes), or ' 'only the ones where the entity itself is changed (no)' ], } return merge_help_strings( get_help_strings(), (group_help, command_help, argument_help), ) # # history show <entity> [yes|no] [n] # default_any_entity = 'yes' default_num_changes = '0' # 0 means no limit all_commands['history_show'] = Command( ('history', 'show'), Id(help_ref='id:target:entity'), YesNo(help_ref='yes_no_all_changes', optional=True, default=default_any_entity), Integer(help_ref='limit_number_of_results', optional=True, default=default_num_changes), fs=FormatSuggestion('%s [%s]: %s', ('timestamp', 'change_by', 'message')), perm_filter='can_show_history', ) def history_show(self, operator, entity, any_entity=default_any_entity, limit_number_of_results=default_num_changes): """ show audit log for a given entity. """ ent = self.util.get_target(entity, restrict_to=[]) self.ba.can_show_history(operator.get_entity_id(), ent) ret = [] try: n = int(limit_number_of_results) except ValueError: raise CerebrumError('Illegal range limit, must be an integer: ' '{}'.format(limit_number_of_results)) record_db = AuditLogAccessor(self.db) rows = list(record_db.search(entities=ent.entity_id)) if self._get_boolean(any_entity): rows.extend(list(record_db.search(targets=ent.entity_id))) rows = sorted(rows, key=lambda r: (r.timestamp, r.record_id)) _process = AuditRecordProcessor() for r in rows[-n:]: processed_row = _process(r) ret.append({ 'timestamp': processed_row.timestamp, 'change_by': processed_row.change_by, 'message': processed_row.message }) return ret
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
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 BofhdEmailRTMixin(BofhdEmailMixinBase): """ RT related functions. """ # TODO: RT ise only in use at UiO. This class has not been tested. # TODO: We should probably assert that BofhdCommonBase abd BofhdEmailMixin # is in the MRO. Nothing will work otherwise. default_email_rt_commands = {} # # RT settings # # Pipe function for RT _rt_pipe = '|%s --action %s --queue %s --url %s' % ( '/local/bin/rt-mailgate', '%(action)s', '%(queue)s', 'https://%(host)s/') # This assumes that the only RE meta character in _rt_pipe is the # leading pipe. _rt_patt = "^\\" + _rt_pipe % { 'action': '(\S+)', 'queue': '(\S+)', 'host': '(\S+)' } + "$" # # Helper functions # def _resolve_rt_name(self, queuename): """Return queue and host of RT queue as tuple.""" if queuename.count('@') == 0: # Use the default host return queuename, "rt.uio.no" elif queuename.count('@') > 1: raise CerebrumError("Invalid RT queue name: %s" % queuename) return queuename.split('@') def __get_all_related_rt_targets(self, address): """ Locate and return all ETs associated with the RT queue. Given any address associated with a RT queue, this method returns all the ETs associated with that RT queue. E.g.: 'foo@domain' will return 'foo@domain' and 'foo-comment@queuehost' If address (EA) is not associated with a RT queue, this method raises an exception. Otherwise a list of ET entity_ids is returned. @type address: basestring @param address: One of the mail addresses associated with a RT queue. @rtype: sequence (of ints) @return: A sequence with entity_ids of all ETs related to the RT queue that address is related to. """ et = Email.EmailTarget(self.db) queue, host = self._get_rt_queue_and_host(address) targets = set([]) for action in ("correspond", "comment"): alias = self._rt_pipe % { 'action': action, 'queue': queue, 'host': host, } try: et.clear() et.find_by_alias(alias) except Errors.NotFoundError: continue targets.add(et.entity_id) if not targets: raise CerebrumError("RT queue %s on host %s not found" % (queue, host)) return targets def _get_rt_email_target(self, queue, host): """ Get EmailTarget for an RT queue. """ et = Email.EmailTarget(self.db) try: et.find_by_alias(self._rt_pipe % { 'action': "correspond", 'queue': queue, 'host': host, }) except Errors.NotFoundError: raise CerebrumError("Unknown RT queue %s on host %s" % (queue, host)) return et def _get_rt_queue_and_host(self, address): """ Get RT queue and host. """ et, addr = self._get_email_target_and_address(address) try: m = re.match(self._rt_patt, et.get_alias()) return m.group(2), m.group(3) except AttributeError: raise CerebrumError("Could not get queue and host for %s" % address) # # email rt_create queue[@host] address [force] # default_email_rt_commands['email_rt_create'] = Command( ("email", "rt_create"), RTQueue(), EmailAddress(), YesNo(help_ref="yes_no_force", optional=True), perm_filter='can_rt_create') def email_rt_create(self, operator, queuename, addr, force="No"): """ Create rt queue. """ queue, host = self._resolve_rt_name(queuename) rt_dom = self._get_email_domain_from_str(host) op = operator.get_entity_id() self.ba.can_rt_create(op, domain=rt_dom) try: self._get_rt_email_target(queue, host) except CerebrumError: pass else: raise CerebrumError("RT queue %s already exists" % queuename) addr_lp, addr_domain_name = self._split_email_address(addr) addr_dom = self._get_email_domain_from_str(addr_domain_name) if addr_domain_name != host: self.ba.can_email_address_add(operator.get_entity_id(), domain=addr_dom) replaced_lists = [] # Unusual characters will raise an exception, a too short name # will return False, which we ignore for the queue name. self._is_ok_mailing_list_name(queue) # The submission address is only allowed to be short if it is # equal to the queue name, or the operator is a global # postmaster. if not (self._is_ok_mailing_list_name(addr_lp) or addr == queue + "@" + host or self.ba.is_postmaster(op)): raise CerebrumError("Illegal address for submission: %s" % addr) # Check if list exists and is replaceable try: et, ea = self._get_email_target_and_address(addr) except CerebrumError: pass else: raise CerebrumError("Address <{}> is in use".format(addr)) acc = self._get_account("exim") et = Email.EmailTarget(self.db) ea = Email.EmailAddress(self.db) cmd = self._rt_pipe % { 'action': "correspond", 'queue': queue, 'host': host } et.populate(self.const.email_target_RT, alias=cmd, using_uid=acc.entity_id) et.write_db() # Add primary address ea.populate(addr_lp, addr_dom.entity_id, et.entity_id) ea.write_db() epat = Email.EmailPrimaryAddressTarget(self.db) epat.populate(ea.entity_id, parent=et) epat.write_db() for alias in replaced_lists: if alias == addr: continue lp, dom = self._split_email_address(alias) alias_dom = self._get_email_domain_from_str(dom) ea.clear() ea.populate(lp, alias_dom.entity_id, et.entity_id) ea.write_db() # Add RT internal address if addr_lp != queue or addr_domain_name != host: ea.clear() ea.populate(queue, rt_dom.entity_id, et.entity_id) ea.write_db() # Moving on to the comment address et.clear() cmd = self._rt_pipe % { 'queue': queue, 'action': "comment", 'host': host } et.populate(self.const.email_target_RT, alias=cmd, using_uid=acc.entity_id) et.write_db() ea.clear() ea.populate("%s-comment" % queue, rt_dom.entity_id, et.entity_id) ea.write_db() msg = "RT queue %s on %s added" % (queue, host) if replaced_lists: msg += ", replacing mailing list(s) %s" % ", ".join(replaced_lists) addr = queue + "@" + host self._register_spam_settings(addr, self.const.email_target_RT) self._register_filter_settings(addr, self.const.email_target_RT) return msg # # email rt_delete queue[@host] # default_email_rt_commands['email_rt_delete'] = Command( ("email", "rt_delete"), EmailAddress(), fs=FormatSuggestion([("Deleted address: %s", ("address", ))]), perm_filter='can_rt_delete') def email_rt_delete(self, operator, queuename): """ Delete RT list. """ queue, host = self._resolve_rt_name(queuename) rt_dom = self._get_email_domain_from_str(host) self.ba.can_rt_delete(operator.get_entity_id(), domain=rt_dom) et = Email.EmailTarget(self.db) ea = Email.EmailAddress(self.db) epat = Email.EmailPrimaryAddressTarget(self.db) result = [] for target_id in self.__get_all_related_rt_targets(queuename): try: et.clear() et.find(target_id) except Errors.NotFoundError: continue epat.clear() try: epat.find(et.entity_id) except Errors.NotFoundError: pass else: epat.delete() for r in et.get_addresses(): addr = '%(local_part)s@%(domain)s' % r ea.clear() ea.find_by_address(addr) ea.delete() result.append({'address': addr}) et.delete() return result # # email rt_add_address queue[@host] address # default_email_rt_commands['email_rt_add_address'] = Command( ('email', 'rt_add_address'), RTQueue(), EmailAddress(), perm_filter='can_rt_address_add') def email_rt_add_address(self, operator, queuename, address): """ RT add address. """ queue, host = self._resolve_rt_name(queuename) rt_dom = self._get_email_domain_from_str(host) self.ba.can_rt_address_add(operator.get_entity_id(), domain=rt_dom) et = self._get_rt_email_target(queue, host) lp, dom = self._split_email_address(address) ed = self._get_email_domain_from_str(dom) if host != dom: self.ba.can_email_address_add(operator.get_entity_id(), domain=ed) ea = Email.EmailAddress(self.db) try: ea.find_by_local_part_and_domain(lp, ed.entity_id) raise CerebrumError("Address already exists (%s)" % address) except Errors.NotFoundError: pass if not (self._is_ok_mailing_list_name(lp) or self.ba.is_postmaster(operator.get_entity_id())): raise CerebrumError("Illegal queue address: %s" % address) ea.clear() ea.populate(lp, ed.entity_id, et.entity_id) ea.write_db() return ("OK, added '%s' as e-mail address for '%s'" % (address, queuename)) # # email rt_remove_address queue address # default_email_rt_commands['email_rt_remove_address'] = Command( ('email', 'rt_remove_address'), RTQueue(), EmailAddress(), perm_filter='can_email_address_delete') def email_rt_remove_address(self, operator, queuename, address): """ RT remove address. """ queue, host = self._resolve_rt_name(queuename) rt_dom = self._get_email_domain_from_str(host) self.ba.can_rt_address_remove(operator.get_entity_id(), domain=rt_dom) et = self._get_rt_email_target(queue, host) return self._remove_email_address(et, address) # # email rt_primary_address address # default_email_rt_commands['email_rt_primary_address'] = Command( ("email", "rt_primary_address"), RTQueue(), EmailAddress(), fs=FormatSuggestion([("New primary address: '%s'", ("address", ))]), perm_filter="can_rt_address_add") def email_rt_primary_address(self, operator, queuename, address): """ RT set primary address. """ queue, host = self._resolve_rt_name(queuename) self.ba.can_rt_address_add( operator.get_entity_id(), domain=self._get_email_domain_from_str(host)) rt = self._get_rt_email_target(queue, host) et, ea = self._get_email_target_and_address(address) if rt.entity_id != et.entity_id: raise CerebrumError( "Address <%s> is not associated with RT queue %s" % (address, queuename)) return self._set_email_primary_address(et, ea, address)
class BofhdExtension(BofhdCommonMethods): """ Commands for getting, setting and unsetting consent. """ hidden_commands = {} # Not accessible through bofh all_commands = {} parent_commands = False authz = ConsentAuth def __init__(self, *args, **kwargs): """ """ super(BofhdExtension, self).__init__(*args, **kwargs) # POST: for attr in ('ConsentType', 'EntityConsent'): if not hasattr(self.const, attr): raise RuntimeError('consent: Missing consent constant types') @classmethod def get_help_strings(cls): """ Help strings for consent commands. """ group, cmd, args = get_help_strings() group.setdefault('consent', 'Commands for handling consents') cmd.setdefault('consent', dict()).update({ 'consent_set': cls.consent_set.__doc__, 'consent_unset': cls.consent_unset.__doc__, 'consent_info': cls.consent_info.__doc__, 'consent_list': cls.consent_list.__doc__, }) args.update({ 'consent_type': [ 'type', 'Enter consent type', "'consent list' lists defined consents" ], }) return (group, cmd, args) def check_consent_support(self, entity): """ Assert that entity has EntityConsentMixin. :param Cerebrum.Entity entity: The entity to check. :raise NotImplementedError: If entity lacks consent support. """ entity_type = self.const.EntityType(entity.entity_type) if not isinstance(entity, EntityConsentMixin): raise NotImplementedError( "Entity type '%s' does not support consent." % six.text_type(entity_type)) def _get_consent(self, consent_ident): u""" Get consent constant from constant strval or intval. :type consent_ident: int, str, Cerebrum.Constant.ConstantCode :param consent_ident: Something to lookup a consent constant by :return tuple: A tuple consisting of a EntityConsent constant and its ConsentType constant. :raise Cerebrum.Error.NotFoundError: If the constant cannot be found. """ consent = self.const.human2constant( consent_ident, const_type=self.const.EntityConsent) if not consent: raise CerebrumError("No consent %r" % consent_ident) consent_type = self.const.ConsentType(consent.consent_type) return (consent, consent_type) # # consent set <ident> <consent_type> # all_commands['consent_set'] = Command( ('consent', 'set'), Id(help_ref="id:target:account"), ConsentType(), fs=FormatSuggestion( "OK: Set consent '%s' (%s) for %s '%s' (entity_id=%s)", ('consent_name', 'consent_type', 'entity_type', 'entity_name', 'entity_id')), perm_filter='can_set_consent') def consent_set(self, operator, entity_ident, consent_ident): """ Set a consent for an entity. """ entity = self.util.get_target(entity_ident, restrict_to=[]) self.ba.can_set_consent(operator.get_entity_id(), entity) self.check_consent_support(entity) consent, consent_type = self._get_consent(consent_ident) entity_name = self._get_entity_name(entity.entity_id, entity.entity_type) entity.set_consent(consent) entity.write_db() return { 'consent_name': six.text_type(consent), 'consent_type': six.text_type(consent_type), 'entity_id': entity.entity_id, 'entity_type': six.text_type(self.const.EntityType(entity.entity_type)), 'entity_name': entity_name, } # # consent unset <ident> <consent_type> # all_commands['consent_unset'] = Command( ('consent', 'unset'), Id(help_ref="id:target:account"), ConsentType(), fs=FormatSuggestion( "OK: Removed consent '%s' (%s) for %s '%s' (entity_id=%s)", ('consent_name', 'consent_type', 'entity_type', 'entity_name', 'entity_id')), perm_filter='can_unset_consent') def consent_unset(self, operator, entity_ident, consent_ident): """ Remove a previously set consent. """ entity = self.util.get_target(entity_ident, restrict_to=[]) self.ba.can_unset_consent(operator.get_entity_id(), entity) self.check_consent_support(entity) consent, consent_type = self._get_consent(consent_ident) entity_name = self._get_entity_name(entity.entity_id, entity.entity_type) entity.remove_consent(consent) entity.write_db() return { 'consent_name': six.text_type(consent), 'consent_type': six.text_type(consent_type), 'entity_id': entity.entity_id, 'entity_type': six.text_type(self.const.EntityType(entity.entity_type)), 'entity_name': entity_name, } # # consent info <ident> # all_commands['consent_info'] = Command( ('consent', 'info'), Id(help_ref="id:target:account"), fs=FormatSuggestion( '%-15s %-8s %-17s %-17s %s', ('consent_name', 'consent_type', format_datetime('consent_time_set'), format_datetime('consent_time_expire'), 'consent_description'), hdr='%-15s %-8s %-17s %-17s %s' % ('Name', 'Type', 'Set at', 'Expires at', 'Description')), perm_filter='can_show_consent_info') def consent_info(self, operator, ident): u""" View all set consents for a given entity. """ entity = self.util.get_target(ident, restrict_to=[]) self.check_consent_support(entity) self.ba.can_show_consent_info(operator.get_entity_id(), entity) consents = [] for row in entity.list_consents(entity_id=entity.entity_id, filter_expired=False): consent, consent_type = self._get_consent(int(row['consent_code'])) consents.append({ 'consent_name': six.text_type(consent), 'consent_type': six.text_type(consent_type), 'consent_time_set': row['time_set'], 'consent_time_expire': row['expiry'], 'consent_description': row['description'], }) if not consents: name = self._get_entity_name(entity.entity_id, entity.entity_type) raise CerebrumError( "'%s' (entity_type=%s, entity_id=%s) has no consents set" % (name, six.text_type(self.const.EntityType( entity.entity_type)), entity.entity_id)) return consents # # consent list # all_commands['consent_list'] = Command( ('consent', 'list'), fs=FormatSuggestion( '%-15s %-8s %s', ('consent_name', 'consent_type', 'consent_description'), hdr='%-16s %-9s %s' % ('Name', 'Type', 'Description')), perm_filter='can_list_consents') def consent_list(self, operator): """ List all consent types. """ self.ba.can_list_consents(operator.get_entity_id()) consents = [] for consent in self.const.fetch_constants(self.const.EntityConsent): consent, consent_type = self._get_consent(int(consent)) consents.append({ 'consent_name': six.text_type(consent), 'consent_type': six.text_type(consent_type), 'consent_description': consent.description, }) if not consents: raise CerebrumError("No consent types defined yet") return consents
class BofhdExtension(BofhdCommandBase): """Class with 'user create_unpersonal' method.""" all_commands = {} authz = BofhdUnpersonalAuth @classmethod def get_help_strings(cls): const = Factory.get('Constants')() account_types = const.fetch_constants(const.Account) cmd_args = {} list_sep = '\n - ' for key, value in CMD_ARGS.items(): cmd_args[key] = value[:] if key == 'unpersonal_account_type': cmd_args[key][2] += '\nValid account types:' cmd_args[key][2] += list_sep + list_sep.join( six.text_type(c) for c in account_types) del const return merge_help_strings( ({}, {}, cmd_args), # We want _our_ cmd_args to win! get_help_strings(), ({}, CMD_HELP, {})) # # user create_unpersonal # all_commands['user_create_unpersonal'] = Command( ('user', 'create_unpersonal'), AccountName(), GroupName(), EmailAddress(), SimpleString(help_ref="unpersonal_account_type"), fs=FormatSuggestion("Created account_id=%i", ("account_id",)), perm_filter='can_create_user_unpersonal') def user_create_unpersonal(self, operator, account_name, group_name, contact_address, account_type): """Bofh command: user create_unpersonal""" self.ba.can_create_user_unpersonal(operator.get_entity_id(), group=self._get_group(group_name)) account_type = self._get_constant(self.const.Account, account_type, "account type") account_policy = AccountPolicy(self.db) try: account = account_policy.create_group_account( operator.get_entity_id(), account_name, self._get_group(group_name), contact_address, account_type ) except InvalidAccountCreationArgument as e: raise CerebrumError(e) self._user_password(operator, account) # TBD: Better way of checking if email forwards are in use, by # checking if bofhd command is available? if hasattr(self, '_email_create_forward_target'): localaddr = '{}@{}'.format( account_name, Email.get_primary_default_email_domain()) self._email_create_forward_target(localaddr, contact_address) return {'account_id': int(account.entity_id)}
class BofhdExtension(BofhdCommonMethods): """Commands for managing Feide services and multifactor authentication.""" hidden_commands = {} # Not accessible through bofh all_commands = {} parent_commands = False authz = BofhdFeideAuth def _find_service(self, service_name): fse = FeideService(self.db) try: fse.find_by_name(service_name) except Errors.NotFoundError: raise CerebrumError('No such Feide service') return fse @classmethod def get_help_strings(cls): """ Help strings for Feide commands. """ return (HELP_FEIDE_GROUP, HELP_FEIDE_CMDS, HELP_FEIDE_ARGS) # # feide service_add # all_commands['feide_service_add'] = Command( ('feide', 'service_add'), Integer(help_ref='feide_service_id'), SimpleString(help_ref='feide_service_name'), perm_filter='is_superuser') def feide_service_add(self, operator, feide_id, service_name): """ Add a Feide service """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may add Feide services') if not feide_id.isdigit(): raise CerebrumError('Feide ID can only contain digits.') fse = FeideService(self.db) service_name = service_name.strip() name_error = fse.illegal_name(service_name) if name_error: raise CerebrumError(name_error) for service in fse.search(): if int(feide_id) == int(service['feide_id']): raise CerebrumError( 'A Feide service with that ID already exists') if service_name == service['name']: raise CerebrumError( 'A Feide service with that name already exists') fse.populate(feide_id, service_name) fse.write_db() return "Added Feide service '{}'".format(service_name) # # feide service_remove # all_commands['feide_service_remove'] = Command( ('feide', 'service_remove'), SimpleString(help_ref='feide_service_name'), YesNo(help_ref='feide_service_confirm_remove'), perm_filter='is_superuser') def feide_service_remove(self, operator, service_name, confirm): """ Remove a Feide service. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may remove Feide services') if not confirm: return 'No action taken.' service_name = service_name.strip() fse = self._find_service(service_name) fse.delete() fse.write_db() return "Removed Feide service '{}'".format(service_name) # # feide service_list # all_commands['feide_service_list'] = Command( ('feide', 'service_list'), fs=FormatSuggestion( '%-12d %-12d %s', ('service_id', 'feide_id', 'name'), hdr='%-12s %-12s %s' % ('Entity ID', 'Feide ID', 'Name')), perm_filter='is_superuser') def feide_service_list(self, operator): """ List Feide services. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied('Only superusers may list Feide services') fse = FeideService(self.db) return map(dict, fse.search()) # # feide authn_level_add # all_commands['feide_authn_level_add'] = Command( ('feide', 'authn_level_add'), SimpleString(help_ref='feide_service_name'), SimpleString(help_ref='feide_authn_entity_target'), Integer(help_ref='feide_authn_level'), perm_filter='is_superuser') def feide_authn_level_add(self, operator, service_name, target, level): """ Add an authentication level for a given service and entity. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may add Feide authentication levels') if not level.isdigit() or int(level) not in (3, 4): raise CerebrumError('Authentication level must be 3 or 4') service_name = service_name.strip() fse = self._find_service(service_name) # Allow authentication levels for persons and groups entity = self.util.get_target(target, default_lookup='person', restrict_to=['Person', 'Group']) if entity.search_authn_level(service_id=fse.entity_id, entity_id=entity.entity_id, level=level): raise CerebrumError( 'Authentication level {} for {} for service {} ' 'already enabled'.format(level, target, service_name)) entity.add_authn_level(service_id=fse.entity_id, level=level) return 'Added authentication level {} for {} for {}'.format( level, target, service_name) # # feide authn_level_remove # all_commands['feide_authn_level_remove'] = Command( ('feide', 'authn_level_remove'), SimpleString(help_ref='feide_service_name'), SimpleString(help_ref='feide_authn_entity_target'), Integer(help_ref='feide_authn_level'), perm_filter='is_superuser') def feide_authn_level_remove(self, operator, service_name, target, level): """ Remove an authentication level for a given service and entity. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may remove Feide authentication levels') if not level.isdigit() or int(level) not in (3, 4): raise CerebrumError('Authentication level must be 3 or 4') service_name = service_name.strip() fse = self._find_service(service_name) # Allow authentication levels for persons and groups entity = self.util.get_target(target, default_lookup='person', restrict_to=['Person', 'Group']) if not entity.search_authn_level(service_id=fse.entity_id, entity_id=entity.entity_id, level=level): raise CerebrumError( 'No such authentication level {} for {} for service {}'.format( level, target, service_name)) entity.remove_authn_level(service_id=fse.entity_id, level=level) return 'Removed authentication level {} for {} for {}'.format( level, target, service_name) # # feide authn_level_search # all_commands['feide_authn_level_list'] = Command( ('feide', 'authn_level_list'), SimpleString(help_ref='feide_service_name'), fs=FormatSuggestion( '%-20s %-6d %s', ('service_name', 'level', 'entity'), hdr='%-20s %-6s %s' % ('Service', 'Level', 'Entity')), perm_filter='is_superuser') def feide_authn_level_list(self, operator, service_name): """ List all authentication levels for a service. """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied( 'Only superusers may list Feide authentication levels') service_name = service_name.strip() fse = self._find_service(service_name) en = Factory.get('Entity')(self.db) fsal = FeideServiceAuthnLevelMixin(self.db) result = [] for x in fsal.search_authn_level(service_id=fse.entity_id): try: en.clear() en.find(x['entity_id']) entity_type = six.text_type( self.const.map_const(en.entity_type)) entity_name = self._get_entity_name(en.entity_id, en.entity_type) entity = '{} {} (id:{:d})'.format(entity_type, entity_name, en.entity_id) except: entity = 'id:{}'.format(x['entity_id']) result.append({ 'service_name': service_name, 'level': x['level'], 'entity': entity }) return result
class BofhdExtension(BofhdCommandBase): """The BofhdExctension for AD related commands and functionality.""" all_commands = {} parent_commands = False authz = BofhdAuth @classmethod def get_help_strings(cls): group_help = { 'ad': "Commands for AD related functionality", } # The texts in command_help are automatically line-wrapped, and should # not contain \n command_help = { 'ad': { 'ad_attributetypes': 'List out all defined AD-attribute types', 'ad_list_attributes': 'List all attributes related to given spread', 'ad_info': 'Get AD-related information about an entity', 'ad_set_attribute': 'Set a given AD-attribute for an entity', 'ad_remove_attribute': 'Remove a given AD-attribute for an entity', }, } arg_help = { 'attr_type': ['attr_type', 'Type of AD-attribute', 'See "ad attributetypes" for a list of defined types'], 'attr_value': ['value', 'Value of AD-attribute', 'A string value for the AD-attribute'], 'id': ['id', 'Id', 'The identity of an entity'], 'spread': ['spread', 'Spread', 'The spread for the attribute'], } return (group_help, command_help, arg_help) # ad_attributetypes all_commands['ad_attributetypes'] = Command( ("ad", "attributetypes"), fs=FormatSuggestion( "%-14s %-8s %s", ('name', 'multi', 'desc'), hdr="%-14s %-8s %s" % ('Name', 'Multival', 'Description') ) ) def ad_attributetypes(self, operator): """List out all types of AD-attributes defined in Cerebrum.""" return [ { 'name': six.text_type(c), 'multi': c.multivalued, 'desc': c.description, } for c in self.const.fetch_constants(self.const.ADAttribute) ] # # ad_list_attributes # all_commands['ad_list_attributes'] = Command( ("ad", "list_attributes"), AttributeType(optional=True), Spread(optional=True), fs=FormatSuggestion( "%-20s %-20s %-20s %s", ('attr_type', 'spread', 'entity', 'value'), hdr="%-20s %-20s %-20s %s" % ('Attribute', 'Spread', 'Entity', 'Value') ), perm_filter='is_superuser' ) # TODO: fix BA! def ad_list_attributes(self, operator, attr_type=None, spread=None): """List all attributes, limited to given input.""" if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Only for superusers, for now") # TODO: check if operator has access to the entity atr = spr = None if attr_type: atr = _get_attr(self.const, attr_type) if spread: spr = _get_spread(self.const, spread) ent = EntityADMixin(self.db) return [ { 'attr_type': six.text_type( self.const.ADAttribute(row['attr_code'])), 'spread': six.text_type( self.const.Spread(row['spread_code'])), 'entity': self._get_entity_name(row['entity_id']), 'value': row['value'], } for row in ent.list_ad_attributes(spread=spr, attribute=atr) ] # # ad_info # all_commands['ad_info'] = Command( ("ad", "info"), EntityType(), Id(), fs=FormatSuggestion([ ('AD-id: %-12s %s', ('id_type', 'ad_id')), ('%-20s %-20s %s', ('spread', 'attr_type', 'attr_value'), '%-20s %-20s %s' % ('Spread', 'Attribute', 'Value')), ]), perm_filter='is_superuser' ) def ad_info(self, operator, entity_type, ident): """Return AD related information about a given entity.""" if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Only for superusers, for now") ent = self._get_entity(entity_type, ident) # TODO: check if operator has access to the entity? ret = [] for row in ent.search_external_ids( source_system=self.const.system_ad, entity_id=ent.entity_id, fetchall=False): ret.append({ 'id_type': six.text_type( self.const.EntityExternalId(row['id_type'])), 'ad_id': row['external_id'], }) for row in ent.list_ad_attributes(entity_id=ent.entity_id): ret.append({ 'attr_type': six.text_type( self.const.ADAttribute(row['attr_code'])), 'spread': six.text_type( self.const.Spread(row['spread_code'])), 'attr_value': row['value'], }) return ret # # ad_set_attribute <entity-type> <entity-id> <attr> <spread> <value> # all_commands['ad_set_attribute'] = Command( ("ad", "set_attribute"), EntityType(), Id(), AttributeType(), Spread(), SimpleString(help_ref='attr_value'), fs=FormatSuggestion([ ("AD-attribute %s set for %s, limited to spread %s: %s", ('attribute', 'entity_name', 'spread', 'value')), ('WARNING: %s', ('warning', )) ]), perm_filter='is_superuser' ) # TODO: fix BA! def ad_set_attribute(self, operator, entity_type, ident, attr_type, spread, value): """Set an attribute for a given entity.""" if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Only for superusers, for now") # TODO: check if operator has access to the entity ent = self._get_entity(entity_type, ident) atr = _get_attr(self.const, attr_type) spr = _get_spread(self.const, spread) ent.set_ad_attribute(spread=spr, attribute=atr, value=value) ent.write_db() # We keep using the constant strvals: spread = six.text_type(spr) attr_type = six.text_type(atr) # Check if the spread and attribute is defined for an AD-sync. If not, # add a warning to the output. retval = [{ 'attribute': attr_type, 'entity_name': self._get_entity_name(ent.entity_id), 'entity_type': six.text_type(ent.entity_type), 'spread': spread, 'value': value, }] config = getattr(cereconf, 'AD_SPREADS', None) if config: if spread not in config: retval.append({ 'warning': 'No AD-sync defined for spread: %s' % spread, }) elif attr_type not in config[spread].get('attributes', ()): retval.append({ 'warning': 'AD-sync for %s does not know of: %s' % ( attr_type, spread), }) return retval # # ad_remove_attribute <entity-type> <entity-id> <attr> <spread> # all_commands['ad_remove_attribute'] = Command( ("ad", "remove_attribute"), EntityType(), Id(), AttributeType(), Spread(), fs=FormatSuggestion( "AD-attribute %s removed for %s, for spread %s", ('attribute', 'entity_name', 'spread') ), perm_filter='is_superuser' ) # TODO: fix BA! def ad_remove_attribute(self, operator, entity_type, id, attr_type, spread): """Remove an AD-attribute for a given entity.""" if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Only for superusers, for now") ent = self._get_entity(entity_type, id) atr = _get_attr(self.const, attr_type) spr = _get_spread(self.const, spread) try: ent.delete_ad_attribute(spread=spr, attribute=atr) except Errors.NotFoundError: raise CerebrumError( '%s does not have AD-attribute %s with spread %s' % (ent.entity_id, six.text_type(atr), six.text_type(spr)) ) ent.write_db() return { 'attribute': six.text_type(atr), 'entity_name': self._get_entity_name(ent.entity_id), 'entity_type': six.text_type(ent.entity_type), 'spread': six.text_type(spr), }
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 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 BofhdEmailSympaMixin(BofhdEmailListMixinBase): """ Email list related commands for the Sympa mailing list system. """ default_sympa_commands = {} # TODO: Move all sympa specific methods and attributes here. # SYMPA SETTINGS # # aliases that we must create for each sympa mailing list. # -request, -editor, -owner, -subscribe, -unsubscribe all come from sympa # owner- and -admin are the remnants of mailman _sympa_addr2alias = ( # The first one *is* the official/primary name. Don't reshuffle. ('%(local_part)s@%(domain)s', '|SYMPA_QUEUE %(listname)s'), # Owner addresses... ('%(local_part)s-owner@%(domain)s', '|SYMPA_BOUNCEQUEUE %(listname)s'), ('%(local_part)s-admin@%(domain)s', '|SYMPA_BOUNCEQUEUE %(listname)s'), # Request addresses... ('%(local_part)s-request@%(domain)s', '|SYMPA_QUEUE %(local_part)s-request@%(domain)s'), ('owner-%(local_part)s@%(domain)s', '|SYMPA_QUEUE %(local_part)s-request@%(domain)s'), # Editor address... ('%(local_part)s-editor@%(domain)s', '|SYMPA_QUEUE %(local_part)s-editor@%(domain)s'), # Subscribe address... ('%(local_part)s-subscribe@%(domain)s', '|SYMPA_QUEUE %(local_part)s-subscribe@%(domain)s'), # Unsubscribe address... ('%(local_part)s-unsubscribe@%(domain)s', '|SYMPA_QUEUE %(local_part)s-unsubscribe@%(domain)s'), ) _sympa_address_suffixes = ( '-owner', '-admin', '-request', '-editor', '-subscribe', '-unsubscribe', ) _sympa_address_prefixes = ('owner-', ) def _validate_sympa_list(self, listname): """ Check whether L{listname} is the 'official' name for a sympa ML. Raises CerebrumError if it is not. """ if self._get_sympa_list(listname) != listname: raise CerebrumError("%s is NOT the official Sympa list name" % listname) return listname def _get_sympa_list(self, listname): """ Try to return the 'official' sympa mailing list name, if it can at all be derived from listname. The problem here is that some lists are actually called foo-admin@domain (and their admin address is foo-admin-admin@domain). Since the 'official' names are not tagged in any way, we try to guess. The guesswork proceeds as follows: 1) if listname points to a sympa ET that has a primary address, we are done, listname *IS* the official list name 2) if not, then there must be a prefix/suffix (like -request) and if we chop it off, we can checked the chopped off part for being an official sympa list. The chopping off continues until we run out of special prefixes/suffixes. """ ea = Email.EmailAddress(self.db) et = Email.EmailTarget(self.db) epat = Email.EmailPrimaryAddressTarget(self.db) def has_prefix(address): local_part, domain = self._split_email_address(address) return True in [ local_part.startswith(x) for x in self._sympa_address_prefixes ] def has_suffix(address): local_part, domain = self._split_email_address(address) return True in [ local_part.endswith(x) for x in self._sympa_address_suffixes ] def has_primary_to_me(address): try: ea.clear() ea.find_by_address(address) epat.clear() epat.find(ea.get_target_id()) return True except Errors.NotFoundError: return False def I_am_sympa(address, check_suffix_prefix=True): try: ea.clear() ea.find_by_address(address) except Errors.NotFoundError: # If it does not exist, it cannot be sympa return False et.clear() et.find(ea.get_target_id()) if ((not et.email_target_alias) or et.email_target_type != self.const.email_target_Sympa): # if it's not a Sympa ET, address cannot be sympa return False return True not_sympa_error = CerebrumError("%s is not a Sympa list" % listname) # Simplest case -- listname is actually a sympa ML directly. It does # not matter whether it has a funky prefix/suffix. if I_am_sympa(listname) and has_primary_to_me(listname): return listname # However, if listname does not have a prefix/suffix AND it is not a # sympa address with a primary address, them it CANNOT be a sympa # address. if not (has_prefix(listname) or has_suffix(listname)): raise not_sympa_error # There is a funky suffix/prefix. Is listname actually such a # secondary address? Try to chop off the funky part and test. local_part, domain = self._split_email_address(listname) for prefix in self._sympa_address_prefixes: if not local_part.startswith(prefix): continue lp_tmp = local_part[len(prefix):] addr_to_test = lp_tmp + "@" + domain try: self._get_sympa_list(addr_to_test) return addr_to_test except CerebrumError: pass for suffix in self._sympa_address_suffixes: if not local_part.endswith(suffix): continue lp_tmp = local_part[:-len(suffix)] addr_to_test = lp_tmp + "@" + domain try: self._get_sympa_list(addr_to_test) return addr_to_test except CerebrumError: pass raise not_sympa_error def _is_mailing_list(self, listname): """ Check whether L{listname} refers to a valid mailing list. :rtype: bool :return: True if listname points to a valid Sympa list, else False. """ # Sympa list? try: self._validate_sympa_list(listname) return True except CerebrumError: pass # Must call super() so that all mixins are checked if super(BofhdEmailSympaMixin, self)._is_mailing_list(listname): return True return False def _get_all_related_maillist_targets(self, address): """ Locate and return all email targets (addresses) for a sympa list. Given any address associated with a mailing list, this method returns all the EmailTarget id's that are associated with that mailing list. See method in superclass for more info. """ et, ea = self._get_email_target_and_address(address) if et.email_target_type != self.const.email_target_Sympa: # Not Sympa list, let the superclass deal with it. return super(BofhdEmailSympaMixin, self)._get_all_related_maillist_targets(address) official_ml_address = self._get_sympa_list(ea.get_address()) patterns = [x[0] for x in self._sympa_addr2alias] return self._get_maillist_targets_for_pattern(official_ml_address, patterns) def _create_sympa_list(self, operator, listname, delivery_host, force=False): """ Create a new Sympa list in cerebrum. """ local_part, domain = self._request_list_localpart_domain(operator, listname, force=force) self._register_sympa_list_addresses(listname, local_part, domain, delivery_host) # register auto spam and filter settings for the list self._register_spam_settings(listname, self.const.email_target_Sympa) self._register_filter_settings(listname, self.const.email_target_Sympa) def _create_sympa_list_alias(self, operator, listname, address, delivery_host, force_alias=False): """Create an alias L{address} for an existing Sympa L{listname}. @type listname: basestring @param listname: Email address for an existing mailing list. This is the ML we are aliasing. @type address: basestring @param address: Email address which will be the alias. @type delivery_host: EmailServer instance or None. @param delivery_host: Host where delivery to the mail alias happens. """ lp, dom = self._split_email_address(address) ed = self._get_email_domain_from_str(dom) self.ba.can_email_list_create(operator.get_entity_id(), ed) self._validate_sympa_list(listname) if not force_alias: try: self._get_account(lp, idtype='name') except CerebrumError: pass else: raise CerebrumError( "Won't create list-alias %s, beause %s is a username" % (address, lp)) # we _don't_ check for "more than 8 characters in local # part OR it contains hyphen" since we assume the people # who have access to this command know what they are doing self._register_sympa_list_addresses(listname, lp, dom, delivery_host) def _register_sympa_list_addresses(self, listname, local_part, domain, delivery_host): """ Register all neccessary sympa addresses. Add list, request, editor, owner, subscribe and unsubscribe addresses to a sympa mailing list. @type listname: basestring @param listname: Sympa listname that the operation is about. listname is typically different from local_part@domain when we are creating an alias. local_part@domain is the alias, listname is the original listname. And since aliases should point to the 'original' ETs, we have to use listname to locate the ETs. @type local_part: basestring @param local_part: See domain @type domain: basestring @param domain: L{local_part} and domain together represent a new list address that we want to create. @type delivery_host: EmailServer instance. @param delivery_host: EmailServer where e-mail to L{listname} is to be delivered through. """ if (delivery_host.email_server_type != self.const.email_server_type_sympa): raise CerebrumError( "Delivery host %s has wrong type (%s) for sympa list %s" % (delivery_host.get_name(self.const.host_namespace), self.const.EmailServerType( delivery_host.email_server_type), listname)) ed = Email.EmailDomain(self.db) ed.find_by_domain(domain) et = Email.EmailTarget(self.db) ea = Email.EmailAddress(self.db) epat = Email.EmailPrimaryAddressTarget(self.db) try: ea.find_by_local_part_and_domain(local_part, ed.entity_id) except Errors.NotFoundError: pass else: raise CerebrumError("The address %s@%s is already in use" % (local_part, domain)) sympa = self._get_account('sympa', idtype='name', actype='PosixUser') primary_ea_created = False listname_lp, listname_domain = listname.split("@") # For each of the addresses we are supposed to create... for pattern, pipe_destination in self._sympa_addr2alias: address = pattern % locals() address_lp, address_domain = address.split("@") # pipe has to be derived from the original listname, since it's # used to locate the ET. pipe = pipe_destination % { 'local_part': listname_lp, 'domain': listname_domain, 'listname': listname } # First check whether the address already exist. It should not. try: ea.clear() ea.find_by_local_part_and_domain(address_lp, ed.entity_id) raise CerebrumError("Can't add list %s as the address %s " "is already in use" % (listname, address)) except Errors.NotFoundError: pass # Then find the target for this particular email address. The # target may already exist, though. et.clear() try: et.find_by_alias_and_account(pipe, sympa.entity_id) except Errors.NotFoundError: et.populate(self.const.email_target_Sympa, alias=pipe, using_uid=sympa.entity_id, server_id=delivery_host.entity_id) et.write_db() # Then create the email address and associate it with the ET. ea.clear() ea.populate(address_lp, ed.entity_id, et.entity_id) ea.write_db() # And finally, the primary address. The first entry in # _sympa_addr2alias will match. Do not reshuffle that tuple! if not primary_ea_created: epat.clear() try: epat.find(et.entity_id) except Errors.NotFoundError: epat.clear() epat.populate(ea.entity_id, parent=et) epat.write_db() primary_ea_created = True def _email_info_sympa(self, operator, et, addr): """ Collect Sympa-specific information for a ML L{addr}. """ def fish_information(suffix, local_part, domain, listname): """Generate an entry for sympa info for the specified address. @type address: basestring @param address: Is the address we are looking for (we locate ETs based on the alias value in _sympa_addr2alias). @type et: EmailTarget instance @rtype: sequence (of dicts of basestring to basestring) @return: A sequence of dicts suitable for merging into return value from email_info_sympa. """ result = [] address = "%(local_part)s-%(suffix)s@%(domain)s" % locals() target_alias = None for a, alias in self._sympa_addr2alias: a = a % locals() if a == address: target_alias = alias % locals() break # IVR 2008-08-05 TBD Is this an error? All sympa ETs must have an # alias in email_target. if target_alias is None: return result try: # Do NOT change et's (parameter's) state. et_tmp = Email.EmailTarget(self.db) et_tmp.clear() et_tmp.find_by_alias(target_alias) except Errors.NotFoundError: return result addrs = et_tmp.get_addresses() if not addrs: return result pattern = '%(local_part)s@%(domain)s' result.append({'sympa_' + suffix + '_1': pattern % addrs[0]}) for idx in range(1, len(addrs)): result.append({'sympa_' + suffix: pattern % addrs[idx]}) return result # end fish_information # listname may be one of the secondary addresses. # email info sympatest@domain MUST be equivalent to # email info sympatest-admin@domain. listname = self._get_sympa_list(addr) ret = [{"sympa_list": listname}] if listname.count('@') == 0: lp, dom = listname, addr.split('@')[1] else: lp, dom = listname.split('@') ed = Email.EmailDomain(self.db) ed.find_by_domain(dom) ea = Email.EmailAddress(self.db) try: ea.find_by_local_part_and_domain(lp, ed.entity_id) except Errors.NotFoundError: raise CerebrumError( "Address %s exists, but the list it points to, %s, does not" % (addr, listname)) # now find all e-mail addresses et_sympa = Email.EmailTarget(self.db) et_sympa.clear() et_sympa.find(ea.email_addr_target_id) addrs = self._get_valid_email_addrs(et_sympa, sort=True) # IVR 2008-08-21 According to postmasters, only superusers should see # forwarding and delivery host information if self.ba.is_postmaster(operator.get_entity_id()): if et_sympa.email_server_id is None: delivery_host = "N/A (this is an error)" else: delivery_host = self._get_email_server( et_sympa.email_server_id).name ret.append({"sympa_delivery_host": delivery_host}) ret += self._email_info_forwarding(et_sympa, addrs) aliases = [] for row in et_sympa.get_addresses(): a = "%(local_part)s@%(domain)s" % row if a == listname: continue aliases.append(a) if aliases: ret.append({"sympa_alias_1": aliases[0]}) for next_alias in aliases[1:]: ret.append({"sympa_alias": next_alias}) for suffix in ("owner", "request", "editor", "subscribe", "unsubscribe"): ret.extend(fish_information(suffix, lp, dom, listname)) return ret # COMMANDS # # sympa create_list <run-host> <delivery-host> <listaddr> <admins> # <profile> <desc> [force?] # default_sympa_commands['sympa_create_list'] = Command( ("sympa", "create_list"), SimpleString(help_ref='string_exec_host'), SimpleString(help_ref='string_email_delivery_host'), EmailAddress(help_ref="mailing_list"), SimpleString(help_ref="mailing_admins"), SimpleString(help_ref="mailing_list_profile"), SimpleString(help_ref="mailing_list_description"), YesNo(help_ref="yes_no_force", optional=True, default="No"), fs=FormatSuggestion([("Sympa list '%s' created", ('listname', ))]), perm_filter="can_email_list_create") def sympa_create_list(self, operator, run_host, delivery_host, listname, admins, list_profile, list_description, yes_no_force="No"): """ Create a sympa list in Cerebrum and on the sympa server(s). Registers all the necessary cerebrum information and make a bofhd request for the actual list creation. """ # Check that the profile is legal if list_profile not in cereconf.SYMPA_PROFILES: raise CerebrumError("Profile %s for sympa list %s is not valid" % (list_profile, listname)) # Check that the command exec host is sane if run_host not in cereconf.SYMPA_RUN_HOSTS: raise CerebrumError("run-host '%s' for list '%s' is not valid" % (run_host, listname)) metachars = "'\"$&()*;<>?[\\]`{|}~\n" def has_meta(s1, s2=metachars): """Check if any char of s1 is in s2""" for c in s1: if c in s2: return True return False # Sympa list creation command will be passed through multiple # exec/shells. Better be restrictive. if True in [ has_meta(x) for x in (run_host, delivery_host, listname, admins, list_profile, list_description) ]: raise CerebrumError( "Illegal metacharacter in list parameter. Allowed: '%s'" % metachars) delivery_host = self._get_email_server(delivery_host) force = self._is_yes(yes_no_force) self._create_sympa_list(operator, listname, delivery_host, force=force) # Now make a bofhd request to create the list itself admin_list = list() for item in admins.split(","): # it's a user name. That username must exist in Cerebrum if "@" not in item: self._get_account(item) # TODO: Not good, this is in use by UIA item = item + "@ulrik.uio.no" admin_list.append(item) # Make the request. lp, dom = self._split_email_address(listname) ed = self._get_email_domain_from_str(dom) ea = Email.EmailAddress(self.db) ea.clear() ea.find_by_local_part_and_domain(lp, ed.entity_id) list_id = ea.entity_id # IVR 2008-08-01 TBD: this is a big ugly. We need to pass several # arguments to p_b_r, but we cannot really store them anywhere :( The # idea is then to take a small dict, pickle it, shove into state_data, # unpickle in p_b_r and be on our merry way. It is at the very best # suboptimal. state = { "runhost": run_host, # IVR 2008-08-01 FIXME: non-fqdn? force? # check? "admins": admin_list, "profile": list_profile, "description": list_description, } br = BofhdRequests(self.db, self.const) # IVR 2009-04-17 +30 minute delay to allow changes to spread to # LDAP. The postmasters are nagging for that delay. All questions # should be directed to them (this is similar to delaying a delete # request). br.add_request(operator.get_entity_id(), DateTime.now() + DateTime.DateTimeDelta(0, 0, 30), self.const.bofh_sympa_create, list_id, ea.entity_id, state_data=pickle.dumps(state)) return {'listname': listname} # # sympa delete_list <run-host> <list-address> # default_sympa_commands['sympa_remove_list'] = Command( ("sympa", "remove_list"), SimpleString(help_ref='string_exec_host'), EmailAddress(help_ref="mailing_list_exist"), YesNo(help_ref="yes_no_with_request"), fs=FormatSuggestion([("Sympa list '%s' deleted (bofhd request: %s)", ( 'listname', 'request', ))]), perm_filter="can_email_list_delete") def sympa_remove_list(self, operator, run_host, listname, force_yes_no): """ Remove a sympa list from cerebrum. @type force_request: bool @param force_request: Controls whether a bofhd request should be issued. This may come in handy, if we want to delete a sympa list from Cerebrum only and not issue any requests. misc cancel_request would have worked too, but it's better to merge this into one command. """ force_request = self._is_yes(force_yes_no) # Check that the command exec host is sane if run_host not in cereconf.SYMPA_RUN_HOSTS: raise CerebrumError("run-host '%s' for list '%s' is not valid" % (run_host, listname)) et, ea = self._get_email_target_and_address(listname) self.ba.can_email_list_delete(operator.get_entity_id(), ea) if et.email_target_type != self.const.email_target_Sympa: raise CerebrumError( "'%s' is not a sympa list (type: %s)" % (listname, self.const.EmailTarget(et.email_target_type))) epat = Email.EmailPrimaryAddressTarget(self.db) list_id = ea.entity_id # Now, there are *many* ETs/EAs associated with one sympa list. We # have to wipe them all out. if not self._validate_sympa_list(listname): raise CerebrumError("Illegal sympa list name: '%s'", listname) deleted_EA = self.email_info(operator, listname) # needed for pattern interpolation below (these are actually used) local_part, domain = self._split_email_address(listname) for pattern, pipe_destination in self._sympa_addr2alias: address = pattern % locals() # For each address, find the target, and remove all email # addresses for that target (there may be many addresses for the # same target). try: ea.clear() ea.find_by_address(address) et.clear() et.find(ea.get_target_id()) epat.clear() try: epat.find(et.entity_id) except Errors.NotFoundError: pass else: epat.delete() # Wipe all addresses... for row in et.get_addresses(): addr = '%(local_part)s@%(domain)s' % row ea.clear() ea.find_by_address(addr) ea.delete() et.delete() except Errors.NotFoundError: pass if cereconf.INSTITUTION_DOMAIN_NAME == 'uio.no': self._report_deleted_EA(deleted_EA) if not force_request: return {'listname': listname, 'request': False} br = BofhdRequests(self.db, self.const) state = {'run_host': run_host, 'listname': listname} br.add_request( operator.get_entity_id(), # IVR 2008-08-04 +1 hour to allow changes to spread to # LDAP. This way we'll have a nice SMTP-error, rather # than a confusing error burp from sympa. DateTime.now() + DateTime.DateTimeDelta(0, 1), self.const.bofh_sympa_remove, list_id, None, state_data=pickle.dumps(state)) return {'listname': listname, 'request': True} # # sympa create_list_alias <list-address> <new-alias> # default_sympa_commands['sympa_create_list_alias'] = Command( ("sympa", "create_list_alias"), EmailAddress(help_ref="mailing_list_exist"), EmailAddress(help_ref="mailing_list"), YesNo(help_ref="yes_no_force", optional=True), fs=FormatSuggestion([("List alias '%s' created", ('alias', ))]), perm_filter="can_email_list_create") def sympa_create_list_alias(self, operator, listname, address, yes_no_force='No'): """ Create a secondary name for an existing Sympa list. """ force = self._is_yes(yes_no_force) # The first thing we have to do is to locate the delivery # host. Postmasters do NOT want to allow people to specify a different # delivery host for alias than for the list that is being aliased. So, # find the ml's ET and fish out the server_id. self._validate_sympa_list(listname) local_part, domain = self._split_email_address(listname) ed = self._get_email_domain_from_str(domain) email_address = Email.EmailAddress(self.db) email_address.find_by_local_part_and_domain(local_part, ed.entity_id) try: delivery_host = self._get_server_from_address(email_address) except CerebrumError: raise CerebrumError("Cannot alias list %s (missing delivery host)", listname) # TODO: Look at perms (are now done by _register) self._create_sympa_list_alias(operator, listname, address, delivery_host, force_alias=force) return { 'target': listname, 'alias': address, } # # email remove_sympa_list_alias <alias> # default_sympa_commands['sympa_remove_list_alias'] = Command( ('sympa', 'remove_list_alias'), EmailAddress(help_ref='mailing_list_alias'), fs=FormatSuggestion([("List alias '%s' removed", ('alias', ))]), perm_filter='can_email_list_create') def sympa_remove_list_alias(self, operator, alias): """ Remove Sympa list aliases. """ lp, dom = self._split_email_address(alias, with_checks=False) ed = self._get_email_domain_from_str(dom) self.ba.can_email_list_create(operator.get_entity_id(), ed) ea = Email.EmailAddress(self.db) et = Email.EmailTarget(self.db) for addr_format, pipe in self._sympa_addr2alias: addr = addr_format % { "local_part": lp, "domain": dom, } try: ea.clear() ea.find_by_address(addr) except Errors.NotFoundError: # Even if one of the addresses is missing, it does not matter # -- we are removing the alias anyway. The right thing to do # here is to continue, as if deletion worked fine. Note that # the ET belongs to the original address, not the alias, so if # we don't delete it when the *alias* is removed, we should # still be fine. continue try: et.clear() et.find(ea.email_addr_target_id) except Errors.NotFoundError: raise CerebrumError("Could not find e-mail target for %s" % addr) # nuke the address, and, if it's the last one, nuke the target as # well. self._remove_email_address(et, addr) return {'alias': alias} # # email_create_sympa_cerebrum_list # TODO: WTF? # default_sympa_commands['sympa_create_list_in_cerebrum'] = Command( ("sympa", "create_list_in_cerebrum"), SimpleString(help_ref='string_email_delivery_host'), EmailAddress(help_ref="mailing_list"), YesNo(help_ref="yes_no_force", optional=True, default="No"), fs=FormatSuggestion([("Sympa list '%s' created (only in Cerebrum)", ('listname', ))]), perm_filter="can_email_list_create") def sympa_create_list_in_cerebrum(self, operator, delivery_host, listname, yes_no_force=None): """ Create a sympa mailing list in cerebrum only. """ delivery_host = self._get_email_server(delivery_host) force = self._is_yes(yes_no_force) self._create_sympa_list(operator, listname, delivery_host, force=force) return {'listname': listname}
class BofhdContactCommands(BofhdCommandBase): all_commands = {} authz = BofhdContactAuth @property def util(self): # TODO: Or should we inherit from BofhdCommonMethods? # We're not really interested in user_delete, etc... try: return self.__util except AttributeError: self.__util = BofhdUtils(self.db) return self.__util @classmethod def get_help_strings(cls): """Get help strings.""" # look up types co = Factory.get('Constants')() source_systems = co.fetch_constants(co.AuthoritativeSystem) contact_types = co.fetch_constants(co.ContactInfo) # Enrich cmd_args with actual constants. # TODO: Find a better way to do this for all similar cmd_args cmd_args = {} list_sep = '\n - ' for k, v in CMD_ARGS.items(): cmd_args[k] = v[:] if k == 'entity_contact_source_system': cmd_args[k][2] += '\nSource systems:' cmd_args[k][2] += list_sep + list_sep.join(six.text_type(c) for c in source_systems) if k == 'entity_contact_type': cmd_args[k][2] += '\nContact types:' cmd_args[k][2] += list_sep + list_sep.join(six.text_type(c) for c in contact_types) del co return merge_help_strings( ({}, {}, cmd_args), # We want _our_ cmd_args to win! get_help_strings(), ({}, CMD_HELP, {})) # # entity contactinfo_add <entity> <contact type> <contact value> # all_commands['entity_contactinfo_add'] = Command( ('entity', 'contactinfo_add'), SimpleString(help_ref='id:target:entity'), SimpleString(help_ref='entity_contact_type'), SimpleString(help_ref='entity_contact_value'), fs=FormatSuggestion( "Added contact info %s:%s '%s' to '%s' with id=%d", ('source_system', 'contact_type', 'contact_value', 'entity_type', 'entity_id') ), perm_filter='can_add_contact_info', ) def entity_contactinfo_add(self, operator, entity_target, contact_type, contact_value): """Manually add contact info to an entity.""" # default values contact_pref = 50 source_system = self.const.system_manual contact_type = contact_type.upper() # get entity object entity = self.util.get_target(entity_target, restrict_to=[]) entity_type = self.const.EntityType(int(entity.entity_type)) if not hasattr(entity, 'get_contact_info'): raise CerebrumError("No support for contact info in %s entity" % six.text_type(entity_type)) # validate contact info type contact_type = self._get_constant(self.const.ContactInfo, contact_type) # check permissions self.ba.can_add_contact_info(operator.get_entity_id(), entity=entity, contact_type=contact_type) # validate email if contact_type == self.const.contact_email: # validate localpart and extract domain. if contact_value.count('@') != 1: raise CerebrumError("Email address (%r) must be on form" "<localpart>@<domain>" % contact_value) localpart, domain = contact_value.split('@') domain = domain.lower() # normalize domain-part ea = Email.EmailAddress(self.db) ed = Email.EmailDomain(self.db) try: if not ea.validate_localpart(localpart): raise AttributeError('Invalid local part') ed._validate_domain_name(domain) # If checks are okay, reassemble email with normalised domain contact_value = localpart + "@" + domain except AttributeError as e: raise CerebrumError(e) # validate phone numbers if contact_type in (self.const.contact_phone, self.const.contact_phone_private, self.const.contact_mobile_phone, self.const.contact_private_mobile): # allows digits and a prefixed '+' if not re.match(r"^\+?\d+$", contact_value): raise CerebrumError( "Invalid phone number: %r. " "The number can contain only digits " "with possible '+' for prefix." % contact_value) # get existing contact info for this entity and contact type contacts = entity.get_contact_info(source=source_system, type=contact_type) existing_prefs = [int(row["contact_pref"]) for row in contacts] for row in contacts: # if the same value already exists, don't add it # case-insensitive check for existing email address if contact_value.lower() == row["contact_value"].lower(): raise CerebrumError("Contact value already exists") # if the value is different, add it with a lower (=greater number) # preference for the new value if contact_pref == row["contact_pref"]: contact_pref = max(existing_prefs) + 1 logger.debug( 'Incremented preference, new value = %d' % contact_pref) logger.debug('Adding contact info: %r, %r, %r, %r', entity.entity_id, six.text_type(contact_type), contact_value, contact_pref) entity.add_contact_info(source_system, type=contact_type, value=contact_value, pref=int(contact_pref), description=None, alias=None) return { 'source_system': six.text_type(source_system), 'contact_type': six.text_type(contact_type), 'contact_value': six.text_type(contact_value), 'entity_type': six.text_type(entity_type), 'entity_id': int(entity.entity_id), } # # entity contactinfo_remove <entity> <source system> <contact type> # all_commands['entity_contactinfo_remove'] = Command( ("entity", "contactinfo_remove"), SimpleString(help_ref='id:target:entity'), SourceSystem(help_ref='entity_contact_source_system'), SimpleString(help_ref='entity_contact_type'), fs=FormatSuggestion([ ("Removed contact info %s:%s from %s with id=%d", ('source_system', 'contact_type', 'entity_type', 'entity_id',)), ("Old value: '%s'", ('contact_value', )), ('Warning: %s', ('warning',)), ]), perm_filter='can_remove_contact_info') def entity_contactinfo_remove(self, operator, entity_target, source_system, contact_type): """Deleting an entity's contact info from a given source system. Useful in cases where the entity has old contact information from a source system he no longer is exported from, i.e. no affiliations.""" co = self.const contact_type = contact_type.upper() # get entity object entity = self.util.get_target(entity_target, restrict_to=[]) entity_type = self.const.EntityType(int(entity.entity_type)) if not hasattr(entity, 'get_contact_info'): raise CerebrumError("No support for contact info in %s entity" % six.text_type(entity_type)) source_system = self._get_constant(self.const.AuthoritativeSystem, source_system) contact_type = self._get_constant(self.const.ContactInfo, contact_type) # check permissions self.ba.can_remove_contact_info(operator.get_entity_id(), entity=entity, contact_type=contact_type, source_system=source_system) warning = None # if the entity is a person... if entity_type == co.entity_person: # check if person is still affiliated with the given source system for a in entity.get_affiliations(): # allow contact info added manually to be removed if (co.AuthoritativeSystem(a['source_system']) is co.system_manual): continue if (co.AuthoritativeSystem(a['source_system']) is source_system): warning = ( 'person has affiliation from source_system %s' % (source_system,)) break # check if given contact info type exists for this entity contact_info = entity.get_contact_info(source=source_system, type=contact_type) if not contact_info: raise CerebrumError( "Entity does not have contact info type %s in %s" % (six.text_type(contact_type), six.text_type(source_system))) else: contact_info = contact_info.pop(0) logger.debug('Removing contact info: %r, %r, %r', entity.entity_id, six.text_type(source_system), six.text_type(contact_type)) try: entity.delete_contact_info(source=source_system, contact_type=contact_type) entity.write_db() except Exception: raise CerebrumError("Could not remove contact info %s:%s from %r" % (six.text_type(source_system), six.text_type(contact_type), entity_target)) result = { 'source_system': six.text_type(source_system), 'contact_type': six.text_type(contact_type), 'entity_type': six.text_type(entity_type), 'entity_id': int(entity.entity_id), } try: self.ba.can_get_contact_info(operator.get_entity_id(), entity=entity, contact_type=contact_type) result['contact_value'] = six.text_type( contact_info['contact_value']) except PermissionDenied: pass if warning: result['warning'] = warning return result # # entity contactinfo_show <entity> # all_commands['entity_contactinfo_show'] = Command( ("entity", "contactinfo_show"), SimpleString(help_ref='id:target:entity'), # SimpleString(help_ref='entity_contact_type'), fs=FormatSuggestion( "%-15s %-15s %-8s %-16s %s", ('source_system', 'contact_type', 'contact_pref', format_time('modified'), 'contact_value', ), hdr="%-15s %-15s %-8s %-16s %s" % ('Source', 'Type', 'Weight', 'Modified', 'Value') ), perm_filter='can_get_contact_info') def entity_contactinfo_show(self, operator, entity_target): """ Show contact info of an entity. """ entity = self.util.get_target(entity_target, restrict_to=[]) entity_type = self.const.EntityType(int(entity.entity_type)) if not hasattr(entity, 'get_contact_info'): raise CerebrumError("No support for contact info in %s entity" % six.text_type(entity_type)) def get_allowed_types(): for contact_type in self.const.fetch_constants( self.const.ContactInfo): try: self.ba.can_get_contact_info(operator.get_entity_id(), entity=entity, contact_type=contact_type) yield contact_type except PermissionDenied: continue contact_types = list(get_allowed_types()) if not contact_types: raise PermissionDenied("Not allowed to see any contact info for %r" % entity_target) contact_info = list() for row in entity.get_contact_info(type=contact_types): contact_info.append({ 'source_system': six.text_type( self.const.AuthoritativeSystem(row['source_system'])), 'contact_type': six.text_type( self.const.ContactInfo(row['contact_type'])), 'contact_pref': six.text_type(row['contact_pref']), 'contact_value': row['contact_value'], 'description': row['description'], 'contact_alias': row['contact_alias'], 'modified': row['last_modified'], }) if not contact_info: raise CerebrumError("No contact info for %r" % entity_target) return contact_info