class BofhdOUDiskMappingCommands(BofhdCommandBase): """OU Disk Mapping commands.""" all_commands = {} authz = BofhdOUDiskMappingAuth @classmethod def get_help_strings(cls): """Get help strings.""" return merge_help_strings((HELP_GROUP, HELP_CMD, HELP_ARGS), get_help_strings()) def __find_ou(self, ou): # Try to find the OU the user wants to edit ou_class = self.OU_class(self.db) if ou.startswith("id:"): ou = ou[3:] # Assume we got the entity id of the ou try: ou_class.find(ou) except Errors.NotFoundError: raise CerebrumError("Unknown OU id {}".format( six.text_type(ou))) elif len(ou) == 6: # Assume we got a stedkode fakultet = ou[:2] institutt = ou[2:4] avdeling = ou[4:] institusjon = cereconf.DEFAULT_INSTITUSJONSNR try: ou_class.find_stedkode(fakultet, institutt, avdeling, institusjon) except Errors.NotFoundError: raise CerebrumError("Unknown OU with stedkode {}".format( six.text_type(ou))) else: raise CerebrumError( "Unable to parse OU id or stedkode {}".format(ou)) return ou_class # # ou homedir_set <path> <ou> <aff> <status> # all_commands["ou_homedir_add"] = Command( ("ou", "homedir_add"), SimpleString(help_ref="path"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "Set homedir='%s' for affiliation %s at OU %i %s", ("path", "aff", "ou", "stedkode"), ), perm_filter="can_add_ou_path", ) def ou_homedir_add(self, operator, disk, ou, aff=None): """Set default home dir for aff at OU :param operator: :param str ou: :param str or None aff: :param str disk: :rtype: dict :return: client output """ ou_class = self.__find_ou(ou) # Check if allowed to set default home dir for this Aff at this OU if not self.ba.can_add_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit aff_str = "*" status = None if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff_str = six.text_type(status) else: aff_str = six.text_type(aff) # Try to find the disk the users want to set disk_class = Factory.get("Disk")(self.db) if disk.startswith("id:") or disk.isdigit(): # Assume this is an entity id try: disk_class.find(disk) except Errors.NotFoundError: raise CerebrumError("Unknown disk with id {}".format( six.text_type(disk))) else: # Assume the user wrote a path try: disk_class.find_by_path(disk) except Errors.NotFoundError: raise CerebrumError("Unknown disk with path {}".format( six.text_type(disk))) # Set the path and return some information to the user ous = OUDiskMapping(self.db) ous.add(ou_class.entity_id, aff, status, disk_class.entity_id) return { "ou": ou_class.entity_id, "aff": aff_str, "path": six.text_type(disk_class.path), "stedkode": "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr( ou_class, 'get_stedkode') else "" } # # ou homedir_clear <ou> <aff> <status> # all_commands["ou_homedir_remove"] = Command( ("ou", "homedir_remove"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "Removed homedir for affiliation %s at OU %i %s", ("aff", "ou", "stedkode"), ), perm_filter="can_remove_ou_path", ) def ou_homedir_remove(self, operator, ou, aff=None): """Remove default home dir for aff at OU If you want to remove the homedir for just the OU and any aff and status, use: > ou homedir_remove ou For OU with affiliation and wildcard status use: > ou homedir_remove ou aff For OU with affiliation and status use: > ou homedir_remove ou aff/status :param operator: :param str ou: :param str or None aff: :rtype: dict :return: client output """ # Try to find the OU the user wants to edit ou_class = self.__find_ou(ou) # Check if allowed to clear home dir for this OU if not self.ba.can_remove_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit aff_str = "*" status = None if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff_str = six.text_type(status) else: aff_str = six.text_type(aff) # Clear the path and return some information to the user ous = OUDiskMapping(self.db) ous.delete(ou_class.entity_id, aff, status) return { "ou": ou_class.entity_id, "aff": aff_str, "stedkode": "with stedkode {} ".format(ou_class.get_stedkode()) if hasattr( ou_class, 'get_stedkode') else "" } # # ou homedir_get <ou> <aff> <status> # all_commands["ou_homedir_list"] = Command( ("ou", "homedir_list"), SimpleString(help_ref="ou"), SimpleString(help_ref="aff", optional=True), fs=FormatSuggestion( "%8s %12i %26s %s", ("stedkode", "ou", "aff", "disk"), hdr="%8s %12s %26s %s" % ("Stedkode", "OU", "Affiliation", "Disk"), ), perm_filter="can_list_ou_path", ) def ou_homedir_list(self, operator, ou, aff=None): """Get default home dir for aff at OU :param operator: :param str ou: :param str or None aff: :rtype: dict :return: client output """ if aff is None: aff = NotSet # Try to find the OU the user wants to edit ou_class = self.__find_ou(ou) # Check if allowed to clear home dir for this OU if not self.ba.can_list_ou_path(operator.get_entity_id(), ou_class.entity_id): raise NO_ACCESS_ERROR # Try to find the Affiliation the user wants to edit if aff: try: aff, status = self.const.get_affiliation(aff) except Errors.NotFoundError: raise CerebrumError('Unknown affiliation {}'.format(aff)) if status: aff = status # Get the path and return some information to the user ous = OUDiskMapping(self.db) disk_class = Factory.get("Disk")(self.db) ret = [] for row in ous.search(ou_class.entity_id, aff, any_status=True): if row["status_code"] is not None: aff_str = str(self.const.PersonAffStatus(row["status_code"])) elif row["aff_code"] is not None: aff_str = str(self.const.PersonAffiliation(row["aff_code"])) else: aff_str = None ou_class.clear() ou_class.find(row["ou_id"]) # Get the stedkode of the OU if the stedkode module is present stedkode = (ou_class.get_stedkode() if hasattr( ou_class, "get_stedkode") else "") disk_class.clear() disk_class.find(row["disk_id"]) ret.append({ "stedkode": stedkode, "ou": row["ou_id"], "aff": aff_str, "disk": disk_class.path, }) return ret
class BofhdExtension(BofhdCommandBase): all_commands = {} hidden_commands = {} authz = RequestsAuth # # misc change_request <request-id> <datetime> # all_commands['misc_change_request'] = Command(("misc", "change_request"), Id(help_ref="id:request_id"), DateTimeString()) def misc_change_request(self, operator, request_id, datetime): if not request_id: raise CerebrumError('Request id required') if not datetime: raise CerebrumError('Date required') datetime = self._parse_date(datetime) br = BofhdRequests(self.db, self.const) old_req = br.get_requests(request_id=request_id) if not old_req: raise CerebrumError("There is no request with id=%r" % request_id) else: # If there is anything, it's at most one old_req = old_req[0] # If you are allowed to cancel a request, you can change it :) self.ba.can_cancel_request(operator.get_entity_id(), request_id) br.delete_request(request_id=request_id) br.add_request(operator.get_entity_id(), datetime, old_req['operation'], old_req['entity_id'], old_req['destination_id'], old_req['state_data']) return "OK, altered request %s" % request_id # # misc list_bofhd_request_types # all_commands['misc_list_bofhd_request_types'] = Command( ("misc", "list_bofhd_request_types"), fs=FormatSuggestion("%-20s %s", ("code_str", "description"), hdr="%-20s %s" % ("Code", "Description"))) def misc_list_bofhd_request_types(self, operator): br = BofhdRequests(self.db, self.const) result = [] for row in br.get_operations(): br_code = self.const.BofhdRequestOp(row['code_str']) result.append({ 'code_str': six.text_type(br_code).lstrip('br_'), 'description': br_code.description, }) return result # # misc cancel_request # all_commands['misc_cancel_request'] = Command( ("misc", "cancel_request"), SimpleString(help_ref='id:request_id')) def misc_cancel_request(self, operator, req): if req.isdigit(): req_id = int(req) else: raise CerebrumError("Request-ID must be a number") br = BofhdRequests(self.db, self.const) if not br.get_requests(request_id=req_id): raise CerebrumError("Request ID %d not found" % req_id) self.ba.can_cancel_request(operator.get_entity_id(), req_id) br.delete_request(request_id=req_id) return "OK, %d canceled" % req_id # # misc list_requests # all_commands['misc_list_requests'] = Command( ("misc", "list_requests"), SimpleString(help_ref='string_bofh_request_search_by', default='requestee'), SimpleString(help_ref='string_bofh_request_target', default='<me>'), fs=FormatSuggestion("%-7i %-10s %-16s %-16s %-10s %-20s %s", ("id", "requestee", format_time("when"), "op", "entity", "destination", "args"), hdr="%-7s %-10s %-16s %-16s %-10s %-20s %s" % ("Id", "Requestee", "When", "Op", "Entity", "Destination", "Arguments"))) def misc_list_requests(self, operator, search_by, destination): br = BofhdRequests(self.db, self.const) ret = [] if destination == '<me>': destination = self._get_account(operator.get_entity_id(), idtype='id') destination = destination.account_name if search_by == 'requestee': account = self._get_account(destination) rows = br.get_requests(operator_id=account.entity_id, given=True) elif search_by == 'operation': try: destination = int( self.const.BofhdRequestOp('br_' + destination)) except Errors.NotFoundError: raise CerebrumError("Unknown request operation %s" % destination) rows = br.get_requests(operation=destination) elif search_by == 'disk': disk_id = self._get_disk(destination)[1] rows = br.get_requests(destination_id=disk_id) elif search_by == 'account': account = self._get_account(destination) rows = br.get_requests(entity_id=account.entity_id) else: raise CerebrumError("Unknown search_by criteria") for r in rows: op = self.const.BofhdRequestOp(r['operation']) dest = None ent_name = None if op in (self.const.bofh_move_user, self.const.bofh_move_request, self.const.bofh_move_user_now): disk = self._get_disk(r['destination_id'])[0] dest = disk.path elif op in (self.const.bofh_move_give, ): dest = self._get_entity_name(r['destination_id'], self.const.entity_group) elif op in (self.const.bofh_email_create, self.const.bofh_email_delete): dest = self._get_entity_name(r['destination_id'], self.const.entity_host) elif op in (self.const.bofh_sympa_create, self.const.bofh_sympa_remove): ea = Email.EmailAddress(self.db) if r['destination_id'] is not None: ea.find(r['destination_id']) dest = ea.get_address() ea.clear() try: ea.find(r['entity_id']) except Errors.NotFoundError: ent_name = "<not found>" else: ent_name = ea.get_address() if ent_name is None: ent_name = self._get_entity_name(r['entity_id'], self.const.entity_account) if r['requestee_id'] is None: requestee = '' else: requestee = self._get_entity_name(r['requestee_id'], self.const.entity_account) ret.append({ 'when': r['run_at'], 'requestee': requestee, 'op': six.text_type(op), 'entity': ent_name, 'destination': dest, 'args': r['state_data'], 'id': r['request_id'] }) ret.sort(lambda a, b: cmp(a['id'], b['id'])) return ret
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
class BofhdJobRunnerCommands(BofhdCommandBase): """Bofh commands for job_runner.""" all_commands = {} authz = BofhdJobRunnerAuth @classmethod def get_help_strings(cls): """Get help strings.""" return CMD_GROUP, CMD_HELP, CMD_ARGS def _run_job_runner_command(self, command, args=None): """Run a job_runner command via the job_runner socket.""" with closing(socket.socket(socket.AF_UNIX)) as sock: sock.connect(cereconf.JOB_RUNNER_SOCKET) sock.settimeout(0.2) try: return SocketProtocol.call(SocketConnection(sock), command, args) except socket.timeout: raise CerebrumError('Error talking to job_runner. Socket ' 'timeout') # # job_runner status # all_commands['job_runner_status'] = Command( ("job_runner", "status"), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_show_job_runner_status') def job_runner_status(self, operator): """Show job runner status.""" # Access control self.ba.can_show_job_runner_status(operator.get_entity_id()) return self._run_job_runner_command('STATUS') # job_runner info # all_commands['job_runner_info'] = Command( ("job_runner", "info"), SimpleString(help_ref='job_runner_name', repeat=False), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_show_job_runner_job') def job_runner_info(self, operator, job_name): """Show job runner status.""" # Access control self.ba.can_show_job_runner_job(operator.get_entity_id()) return self._run_job_runner_command('SHOWJOB', [ job_name, ]) # # job_runner run # all_commands['job_runner_run'] = Command( ("job_runner", "run"), SimpleString(help_ref='job_runner_name', repeat=False), YesNo(help_ref='yes_no_with_deps', optional=True, default="No"), fs=FormatSuggestion("%s", 'simpleString', hdr="%s" % 'Output:'), perm_filter='can_run_job_runner_job') def job_runner_run(self, operator, job_name, with_deps=False): """Run a job runner job.""" # Access control self.ba.can_run_job_runner_job(operator.get_entity_id()) with_deps = self._get_boolean(with_deps) return self._run_job_runner_command('RUNJOB', [job_name, with_deps])
class BofhdExtension(BofhdCommandBase): """Class to expand bofhd with commands for manipulating subnets.""" all_commands = {} parent_commands = False authz = SubnetBofhdAuth def __init__(self, *args, **kwargs): default_zone = getattr(cereconf, 'DNS_DEFAULT_ZONE', kwargs.pop('default_zone', 'uio')) super(BofhdExtension, self).__init__(*args, **kwargs) self.default_zone = self.const.DnsZone(default_zone) @property def _find(self): try: return self.__find_util except AttributeError: self.__find_util = Utils.Find(self.db, self.default_zone) return self.__find_util def _get_subnet_ipv4(self, subnet_identifier): try: s = Subnet(self.db) s.find(subnet_identifier) return s except (ValueError, SubnetError, DNSError): raise CerebrumError("Unable to find subnet %r" % subnet_identifier) def _get_subnet_ipv6(self, subnet_identifier): try: s = IPv6Subnet(self.db) s.find(subnet_identifier) return s except (ValueError, SubnetError, DNSError): raise CerebrumError("Unable to find subnet %r" % subnet_identifier) def _get_subnet(self, subnet_identifier): try: return self._get_subnet_ipv4(subnet_identifier) except CerebrumError: return self._get_subnet_ipv6(subnet_identifier) @classmethod def get_help_strings(cls): _, _, args = get_help_strings() return merge_help_strings( ({}, {}, args), (HELP_SUBNET_GROUP, HELP_SUBNET_CMDS, HELP_SUBNET_ARGS)) # # subnet info <subnet> # all_commands['subnet_info'] = Command( ("subnet", "info"), SubnetIdentifier(), fs=FormatSuggestion([ ("Subnet: %s", ('subnet', )), ("Entity ID: %s", ('entity_id', )), ("Netmask: %s", ('netmask', )), ("Prefix: %s", ("prefix", )), ("Description: '%s'\n" "Name-prefix: '%s'\n" "VLAN: %s\n" "DNS delegated: %s\n" "IP-range: %s", ("desc", "name_prefix", "vlan", "delegated", "ip_range")), ("Reserved host adresses: %s", ("no_of_res_adr", )), ("Reserved addresses: %s", ("res_adr1", )), (" %s", ('res_adr', )), ("Used addresses: %s\n" "Unused addresses: %s (excluding reserved adr.)", ('used', 'unused')), ])) def subnet_info(self, operator, identifier): """Lists the following information about the given subnet: * Subnett * Netmask * Entity ID * Description * Name-prefix * VLAN number * DNS-delegation status * Range of IPs on subnet * Number of reserved addresses * A list of the reserved adresses """ s = self._get_subnet(identifier) is_ipv6 = isinstance(s, IPv6Subnet) ipc = IPv6Calc if is_ipv6 else IPCalc data = { 'subnet': _subnet_to_identifier(s), 'entity_id': str(s.entity_id), 'desc': s.description, 'delegated': "Yes" if s.dns_delegated else "No", 'name_prefix': s.name_prefix, 'no_of_res_adr': str(s.no_of_reserved_adr) } # ipv4 netmask or ipv6 prefix if isinstance(s, Subnet): data['netmask'] = ipc.netmask_to_ip(s.subnet_mask) else: data['prefix'] = '/' + str(s.subnet_mask) if s.vlan_number is not None: data['vlan'] = str(s.vlan_number) else: data['vlan'] = "(None)" data['ip_range'] = "%s - %s" % (ipc.long_to_ip( s.ip_min), ipc.long_to_ip(s.ip_max)) # Calculate number of used and unused IP-addresses on this subnet # ^^^^^^ excluding reserved addresses uip = self._find.count_used_ips(s.subnet_ip) data['used'] = str(uip) data['unused'] = str(s.ip_max - s.ip_min - uip - 1) reserved_adresses = list(sorted(s.reserved_adr)) if reserved_adresses: data["res_adr1"] = "%s (net)" % ipc.long_to_ip( reserved_adresses.pop(0)) else: data["res_adr1"] = "(None)" ret = [ data, ] if reserved_adresses: last_ip = reserved_adresses.pop() for address in reserved_adresses: ret.append({ 'res_adr': ipc.long_to_ip(address), }) ret.append({ 'res_adr': "%s (broadcast)" % ipc.long_to_ip(last_ip), }) return ret # # subnet set_vlan <subnet> <vlan> # all_commands['subnet_set_vlan'] = Command( ("subnet", "set_vlan"), SubnetIdentifier(), Integer(help_ref="subnet_vlan"), fs=FormatSuggestion([ ("OK; VLAN for subnet %s updated from %i to %i", ('subnet_id', 'old_vlan', 'new_vlan')), ]), perm_filter='is_dns_superuser') def subnet_set_vlan(self, operator, identifier, new_vlan): self.ba.assert_dns_superuser(operator.get_entity_id()) try: new_vlan = int(new_vlan) except: raise CerebrumError("VLAN must be an integer; %r isn't" % new_vlan) s = self._get_subnet(identifier) old_vlan = s.vlan_number s.vlan_number = new_vlan s.write_db(perform_checks=False) return { 'subnet_id': _subnet_to_identifier(s), 'old_vlan': old_vlan, 'new_vlan': new_vlan, } # # subnet set_description <subnet> <description> # all_commands['subnet_set_description'] = Command( ("subnet", "set_description"), SubnetIdentifier(), SimpleString(help_ref="subnet_description"), fs=FormatSuggestion([ ("OK; description for subnet %s updated to '%s'", ('subnet_id', 'new_description')), ])) def subnet_set_description(self, operator, identifier, new_description): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) old_description = s.description or '' s.description = new_description s.write_db(perform_checks=False) subnet_id = _subnet_to_identifier(s) return { 'subnet_id': subnet_id, 'old_description': old_description, 'new_description': new_description, } # # subnet set_name_prefix # all_commands['subnet_set_name_prefix'] = Command( ("subnet", "set_name_prefix"), SubnetIdentifier(), SimpleString(help_ref="subnet_name_prefix"), fs=FormatSuggestion([ ("OK; name_prefix for subnet %s updated from '%s' to '%s'", ('subnet_id', 'old_prefix', 'new_prefix')) ]), perm_filter='is_dns_superuser') def subnet_set_name_prefix(self, operator, identifier, new_prefix): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) old_prefix = s.name_prefix s.name_prefix = new_prefix s.write_db(perform_checks=False) subnet_id = self._subnet_to_identifier(s) return { 'subnet_id': subnet_id, 'old_prefix': old_prefix, 'new_prefix': new_prefix, } # # subnet set_dns_delegated # all_commands['subnet_set_dns_delegated'] = Command( ("subnet", "set_dns_delegated"), SubnetIdentifier(), Force(optional=True), fs=FormatSuggestion([ ("Subnet %s set as delegated to external" " DNS server", ('subnet_id', )), ("Note: %s", ('warning', )), ]), perm_filter='is_dns_superuser') def subnet_set_dns_delegated(self, operator, identifier, force=False): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) subnet_id = _subnet_to_identifier(s) if s.dns_delegated: raise CerebrumError("Subnet %s is already set as being delegated" " to external DNS server" % subnet_id) ret = [{ 'subnet_id': subnet_id, }] if s.has_adresses_in_use(): if not force: raise CerebrumError("Subnet '%s' has addresses in use;" " must force to delegate" % subnet_id) ret.append({'warning': "Subnet has address in use!"}) s.dns_delegated = True s.write_db(perform_checks=False) return ret # # subnet unset_dns_delegated # all_commands['subnet_unset_dns_delegated'] = Command( ("subnet", "unset_dns_delegated"), SubnetIdentifier(), fs=FormatSuggestion([ ("Subnet %s no longer set as delegated to external" " DNS server", ('subnet_id', )), ]), perm_filter='is_dns_superuser') def subnet_unset_dns_delegated(self, operator, identifier): self.ba.assert_dns_superuser(operator.get_entity_id()) s = self._get_subnet(identifier) subnet_id = _subnet_to_identifier(s) if not s.dns_delegated: raise CerebrumError("Subnet %s is already set as not being" " delegated to external DNS server" % subnet_id) s.dns_delegated = False s.write_db(perform_checks=False) return { 'subnet_id': subnet_id, } # # subnet set_reserved # all_commands['subnet_set_reserved'] = Command( ("subnet", "set_reserved"), SubnetIdentifier(), Integer(help_ref="subnet_reserved"), fs=FormatSuggestion([ ("OK; Number of reserved addresses for subnet %s " "updated from %i to %i", ('subnet_id', 'old_reserved', 'new_reserved')), ("FIY: %s", ('warning', )), ]), perm_filter='is_dns_superuser') def subnet_set_reserved(self, operator, identifier, new_res): self.ba.assert_dns_superuser(operator.get_entity_id()) try: new_res = int(new_res) except: raise CerebrumError("The number of reserved addresses must be " "an integer; %r isn't" % new_res) if new_res < 0: raise CerebrumError("Cannot set number of reserved addresses to " + "a negative number such as '%s'" % new_res) s = self._get_subnet(identifier) old_res = s.no_of_reserved_adr s.no_of_reserved_adr = new_res s.calculate_reserved_addresses() res = [{ 'subnet_id': _subnet_to_identifier(s), 'old_reserved': old_res, 'new_reserved': new_res, }] if new_res > old_res: try: s.check_reserved_addresses_in_use() except SubnetError as se: res.append({ 'warning': text_type(se), }) s.write_db(perform_checks=False) return res
class BofhdAccessCommands(BofhdCommonMethods): """Bofhd extension with access commands""" all_commands = {} hidden_commands = {} authz = BofhdAccessAuth @classmethod def get_help_strings(cls): return merge_help_strings( super(BofhdAccessCommands, cls).get_help_strings(), (HELP_ACCESS_GROUP, HELP_ACCESS_CMDS, HELP_ACCESS_ARGS)) # # access disk <path> # all_commands['access_disk'] = Command( ('access', 'disk'), DiskId(), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_disk(self, operator, path): disk = self._get_disk(path)[0] result = [] host = Utils.Factory.get('Host')(self.db) try: host.find(disk.host_id) for r in self._list_access("host", host.name, empty_result=[]): if r['attr'] == '' or re.search("/%s$" % r['attr'], path): result.append(r) except Errors.NotFoundError: pass result.extend(self._list_access("disk", path, empty_result=[])) return result or "None" # # access group <group> # all_commands['access_group'] = Command( ('access', 'group'), GroupName(help_ref='group_name_id'), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_group(self, operator, group): return self._list_access("group", group) # # access host <hostname> # all_commands['access_host'] = Command( ('access', 'host'), SimpleString(help_ref="string_host"), fs=FormatSuggestion("%-16s %-16s %-9s %s", ("opset", "attr", "type", "name"), hdr="%-16s %-16s %-9s %s" % ("Operation set", "Pattern", "Type", "Name"))) def access_host(self, operator, host): return self._list_access("host", host) # # access maildom <maildom> # all_commands['access_maildom'] = Command( ('access', 'maildom'), SimpleString(help_ref="email_domain"), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_maildom(self, operator, maildom): # TODO: Is this an email command? Should it be moved to bofhd_email? return self._list_access("maildom", maildom) # # access ou <ou> # all_commands['access_ou'] = Command( ('access', 'ou'), OU(), fs=FormatSuggestion("%-16s %-16s %-9s %s", ("opset", "attr", "type", "name"), hdr="%-16s %-16s %-9s %s" % ("Operation set", "Affiliation", "Type", "Name"))) def access_ou(self, operator, ou): return self._list_access("ou", ou) # # access user <account> # all_commands['access_user'] = Command( ('access', 'user'), AccountName(), fs=FormatSuggestion( "%-14s %-5s %-20s %-7s %-9s %s", ("opset", "target_type", "target", "attr", "type", "name"), hdr="%-14s %-5s %-20s %-7s %-9s %s" % ("Operation set", "TType", "Target", "Attr", "Type", "Name"))) def access_user(self, operator, user): # This is more tricky than the others, we want to show anyone with # access, through OU, host or disk. (not global_XXX, though.) # # Note that there is no auth-type 'account', so you can't be granted # direct access to a specific user. acc = self._get_account(user) # Make lists of the disks and hosts associated with the user disks = {} hosts = {} disk = Utils.Factory.get("Disk")(self.db) for r in acc.get_homes(): # Disk for archived users may not exist anymore try: disk_id = int(r['disk_id']) except TypeError: continue if disk_id not in disks: disk.clear() disk.find(disk_id) disks[disk_id] = disk.path if disk.host_id is not None: basename = disk.path.split("/")[-1] host_id = int(disk.host_id) if host_id not in hosts: hosts[host_id] = [] hosts[host_id].append(basename) # Look through disks ret = [] for d in disks.keys(): for entry in self._list_access("disk", d, empty_result=[]): entry['target_type'] = "disk" entry['target'] = disks[d] ret.append(entry) # Look through hosts: for h in hosts.keys(): for candidate in self._list_access("host", h, empty_result=[]): candidate['target_type'] = "host" candidate['target'] = self._get_host(h).name if candidate['attr'] == "": ret.append(candidate) continue for dir in hosts[h]: if re.match(candidate['attr'], dir): ret.append(candidate) break # TODO: check user's ou(s) ret.sort(lambda x, y: (cmp(x['opset'].lower(), y['opset'].lower()) or cmp(x['name'], y['name']))) return ret # # access global_group # all_commands['access_global_group'] = Command( ('access', 'global_group'), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_global_group(self, operator): return self._list_access("global_group") # # access global_host # all_commands['access_global_host'] = Command( ('access', 'global_host'), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_global_host(self, operator): return self._list_access("global_host") # # access global_maildom # all_commands['access_global_maildom'] = Command( ('access', 'global_maildom'), fs=FormatSuggestion("%-16s %-9s %s", ("opset", "type", "name"), hdr="%-16s %-9s %s" % ("Operation set", "Type", "Name"))) def access_global_maildom(self, operator): return self._list_access("global_maildom") # # access global_ou # all_commands['access_global_ou'] = Command( ('access', 'global_ou'), fs=FormatSuggestion("%-16s %-16s %-9s %s", ("opset", "attr", "type", "name"), hdr="%-16s %-16s %-9s %s" % ("Operation set", "Affiliation", "Type", "Name"))) def access_global_ou(self, operator): return self._list_access("global_ou") # # access global_dns # all_commands['access_global_dns'] = Command( ('access', 'global_dns'), fs=FormatSuggestion("%-16s %-16s %-9s %s", ("opset", "attr", "type", "name"), hdr="%-16s %-16s %-9s %s" % ("Operation set", "Affiliation", "Type", "Name"))) def access_global_dns(self, operator): return self._list_access("global_dns") # TODO: Define all_commands['access_global_dns'] def access_global_person(self, operator): return self._list_access("global_person") # # access grant <opset name> <who> <type> <on what> [<attr>] # all_commands['access_grant'] = Command( ('access', 'grant'), OpSet(), GroupName(help_ref="id:target:group"), EntityType(default='group', help_ref="auth_entity_type"), SimpleString(optional=True, help_ref="auth_target_entity"), SimpleString(optional=True, help_ref="auth_attribute"), perm_filter='can_grant_access') def access_grant(self, operator, opset, group, entity_type, target_name=None, attr=None): return self._manipulate_access(self._grant_auth, operator, opset, group, entity_type, target_name, attr) # # access revoke <opset name> <who> <type> <on what> [<attr>] # all_commands['access_revoke'] = Command( ('access', 'revoke'), OpSet(), GroupName(help_ref="id:target:group"), EntityType(default='group', help_ref="auth_entity_type"), SimpleString(help_ref="auth_target_entity"), SimpleString(optional=True, help_ref="auth_attribute"), perm_filter='can_grant_access') def access_revoke(self, operator, opset, group, entity_type, target_name, attr=None): return self._manipulate_access(self._revoke_auth, operator, opset, group, entity_type, target_name, attr) # # access list_opsets # all_commands['access_list_opsets'] = Command( ('access', 'list_opsets'), fs=FormatSuggestion("%s", ("opset", ), hdr="Operation set")) def access_list_opsets(self, operator): baos = BofhdAuthOpSet(self.db) ret = [] for r in baos.list(): ret.append({'opset': r['name']}) ret.sort(lambda x, y: cmp(x['opset'].lower(), y['opset'].lower())) return ret # # access list_alterable [group] [username] # hidden_commands['access_list_alterable'] = Command( ('access', 'list_alterable'), SimpleString(optional=True), AccountName(optional=True), fs=FormatSuggestion("%s %s", ("entity_name", "description"))) def access_list_alterable(self, operator, target_type='group', access_holder=None): """List entities that access_holder can moderate.""" if access_holder is None: account_id = operator.get_entity_id() else: account = self._get_account(access_holder, actype="PosixUser") account_id = account.entity_id if not (account_id == operator.get_entity_id() or self.ba.is_superuser(operator.get_entity_id())): raise PermissionDenied("You do not have permission for this" " operation") result = [] matches = self.Group_class(self.db).search(admin_id=account_id, admin_by_membership=True) matches += self.Group_class(self.db).search( moderator_id=account_id, moderator_by_membership=True) if len(matches) > cereconf.BOFHD_MAX_MATCHES_ACCESS: raise CerebrumError("More than {:d} ({:d}) matches. Refusing to " "return result".format( cereconf.BOFHD_MAX_MATCHES_ACCESS, len(matches))) for row in matches: try: group = self._get_group(row['group_id']) except Errors.NotFoundError: self.logger.warn( "Non-existent entity (%s) referenced from auth_op_target", row["entity_id"]) continue tmp = { "entity_name": group.group_name, "description": group.description, "expire_date": group.expire_date, } if tmp not in result: result.append(tmp) return result # # access show_opset <opset name> # all_commands['access_show_opset'] = Command( ('access', 'show_opset'), OpSet(), fs=FormatSuggestion("%-16s %-16s %s", ("op", "attr", "desc"), hdr="%-16s %-16s %s" % ("Operation", "Attribute", "Description"))) def access_show_opset(self, operator, opset=None): baos = BofhdAuthOpSet(self.db) try: baos.find_by_name(opset) except Errors.NotFoundError: raise CerebrumError("Unknown operation set: '{}'".format(opset)) ret = [] for r in baos.list_operations(): entry = { 'op': six.text_type(self.const.AuthRoleOp(r['op_code'])), 'desc': self.const.AuthRoleOp(r['op_code']).description, } attrs = [] for r2 in baos.list_operation_attrs(r['op_id']): attrs += [r2['attr']] if not attrs: attrs = [""] for a in attrs: entry_with_attr = entry.copy() entry_with_attr['attr'] = a ret += [entry_with_attr] ret.sort(lambda x, y: (cmp(x['op'], y['op']) or cmp(x['attr'], y['attr']))) return ret # TODO # # To be able to manipulate all aspects of bofhd authentication, we # need a few more commands: # # access create_opset <opset name> # access create_op <opname> <desc> # access delete_op <opname> # access add_to_opset <opset> <op> [<attr>] # access remove_from_opset <opset> <op> [<attr>] # # The opset could be implicitly deleted after the last op was # removed from it. # # access list <owner> [target_type] # all_commands['access_list'] = Command( ('access', 'list'), SimpleString(help_ref='id:target:group'), SimpleString(help_ref='string_perm_target_type_access', optional=True), fs=FormatSuggestion( "%-14s %-16s %-30s %-7s", ("opset", "target_type", "target", "attr"), hdr="%-14s %-16s %-30s %-7s" % ("Operation set", "Target type", "Target", "Attr"))) def access_list(self, operator, owner, target_type=None): """ List everything an account or group can operate on. Only direct ownership is reported: the entities an account can access due to group memberships will not be listed. This does not include unpersonal users owned by groups. :param operator: operator in bofh session :param owner: str name of owner object :param target_type: the type of the target :return: List of everything an account or group can operate on """ ar = BofhdAuthRole(self.db) aot = BofhdAuthOpTarget(self.db) aos = BofhdAuthOpSet(self.db) co = self.const owner_id = self.util.get_target(owner, default_lookup="group", restrict_to=[]).entity_id ret = [] for role in ar.list(owner_id): aos.clear() aos.find(role['op_set_id']) for r in aot.list(target_id=role['op_target_id']): if target_type is not None and r['target_type'] != target_type: continue if r['entity_id'] is None: target_name = "N/A" elif r['target_type'] == co.auth_target_type_maildomain: # FIXME: EmailDomain is not an Entity. ed = Email.EmailDomain(self.db) try: ed.find(r['entity_id']) except (Errors.NotFoundError, ValueError): self.logger.warn("Non-existing entity (e-mail domain) " "in auth_op_target {}:{:d}".format( r['target_type'], r['entity_id'])) continue target_name = ed.email_domain_name elif r['target_type'] == co.auth_target_type_ou: ou = self.OU_class(self.db) try: ou.find(r['entity_id']) except (Errors.NotFoundError, ValueError): self.logger.warn("Non-existing entity (ou) in " "auth_op_target %s:%d" % (r['target_type'], r['entity_id'])) continue target_name = "%02d%02d%02d (%s)" % ( ou.fakultet, ou.institutt, ou.avdeling, ou.short_name) elif r['target_type'] == co.auth_target_type_dns: s = Subnet(self.db) # TODO: should Subnet.find() support ints as input? try: s.find('entity_id:%s' % r['entity_id']) except (Errors.NotFoundError, ValueError, SubnetError): self.logger.warn("Non-existing entity (subnet) in " "auth_op_target %s:%d" % (r['target_type'], r['entity_id'])) continue target_name = "%s/%s" % (s.subnet_ip, s.subnet_mask) else: try: ety = self._get_entity(ident=r['entity_id']) target_name = self._get_name_from_object(ety) except (Errors.NotFoundError, ValueError): self.logger.warn("Non-existing entity in " "auth_op_target %s:%d" % (r['target_type'], r['entity_id'])) continue ret.append({ 'opset': aos.name, 'target_type': r['target_type'], 'target': target_name, 'attr': r['attr'] or "", }) ret.sort(lambda a, b: (cmp(a['target_type'], b['target_type']) or cmp( a['target'], b['target']))) return ret # access dns <dns-target> all_commands['access_dns'] = Command( ('access', 'dns'), SimpleString(), fs=FormatSuggestion("%-16s %-9s %-9s %s", ("opset", "type", "level", "name"), hdr="%-16s %-9s %-9s %s" % ("Operation set", "Type", "Level", "Name"))) def access_dns(self, operator, dns_target): ret = [] if '/' in dns_target: # Asking for rights on subnet; IP not of interest for accessor in self._list_access("dns", dns_target, empty_result=[]): accessor["level"] = "Subnet" ret.append(accessor) else: # Asking for rights on IP; need to provide info about # rights on the IP's subnet too for accessor in self._list_access("dns", dns_target + '/', empty_result=[]): accessor["level"] = "Subnet" ret.append(accessor) for accessor in self._list_access("dns", dns_target, empty_result=[]): accessor["level"] = "IP" ret.append(accessor) return ret # # Helper methods # def _list_access(self, target_type, target_name=None, empty_result="None"): target_id, target_type, target_auth = self._get_access_id( target_type, target_name) ret = [] ar = BofhdAuthRole(self.db) aos = BofhdAuthOpSet(self.db) for r in self._get_auth_op_target(target_id, target_type, any_attr=True): attr = str(r['attr'] or '') for r2 in ar.list(op_target_id=r['op_target_id']): aos.clear() aos.find(r2['op_set_id']) ety = self._get_entity(ident=r2['entity_id']) ret.append({ 'opset': aos.name, 'attr': attr, 'type': six.text_type(self.const.EntityType(ety.entity_type)), 'name': self._get_name_from_object(ety), }) ret.sort(lambda a, b: (cmp(a['opset'], b['opset']) or cmp(a['name'], b['name']))) return ret or empty_result def _manipulate_access(self, change_func, operator, opset, group, entity_type, target_name, attr): """This function does no validation of types itself. It uses _get_access_id() to get a (target_type, entity_id) suitable for insertion in auth_op_target. Additional checking for validity is done by _validate_access(). Those helper functions look for a function matching the target_type, and call it. There should be one _get_access_id_XXX and one _validate_access_XXX for each known target_type. """ opset = self._get_opset(opset) gr = self.util.get_target(group, default_lookup="group", restrict_to=['Account', 'Group']) target_id, target_type, target_auth = self._get_access_id( entity_type, target_name) operator_id = operator.get_entity_id() if target_auth is None and not self.ba.is_superuser(operator_id): raise PermissionDenied("Currently limited to superusers") else: self.ba.can_grant_access(operator_id, target_auth, target_type, target_id, opset) self._validate_access(entity_type, opset, attr) return change_func(gr.entity_id, opset, target_id, target_type, attr, group, target_name) def _get_access_id(self, target_type, target_name): """Get required data for granting access to an operation target. :param str target_type: The type of :rtype: tuple :returns: A three element tuple with information about the operation target: 1. The entity_id of the target entity (int) 2. The target type (str) 3. The `intval` of the operation constant for granting access to the given target entity. """ lookup = LookupClass() if target_type in lookup: lookupclass = lookup[target_type](self.db) return lookupclass.get(target_name) else: raise CerebrumError("Unknown id type {}".format(target_type)) def _validate_access(self, target_type, opset, attr): lookup = LookupClass() if target_type in lookup: lookupclass = lookup[target_type](self.db) return lookupclass.validate(opset, attr) else: raise CerebrumError("Unknown type %s" % target_type) def _revoke_auth(self, entity_id, opset, target_id, target_type, attr, entity_name, target_name): op_target_id = self._get_auth_op_target(target_id, target_type, attr) if not op_target_id: raise CerebrumError( "No one has matching access to {}".format(target_name)) ar = BofhdAuthRole(self.db) rows = ar.list(entity_id, opset.op_set_id, op_target_id) if len(rows) == 0: return "%s doesn't have %s access to %s %s" % ( entity_name, opset.name, six.text_type(target_type), target_name) ar.revoke_auth(entity_id, opset.op_set_id, op_target_id) # See if the op_target has any references left, delete it if not. rows = ar.list(op_target_id=op_target_id) if len(rows) == 0: aot = BofhdAuthOpTarget(self.db) aot.find(op_target_id) aot.delete() return "OK, revoked %s access for %s from %s %s" % ( opset.name, entity_name, six.text_type(target_type), target_name) def _grant_auth(self, entity_id, opset, target_id, target_type, attr, entity_name, target_name): op_target_id = self._get_auth_op_target(target_id, target_type, attr, create=True) ar = BofhdAuthRole(self.db) rows = ar.list(entity_id, opset.op_set_id, op_target_id) if len(rows) == 0: ar.grant_auth(entity_id, opset.op_set_id, op_target_id) return "OK, granted %s access %s to %s %s" % ( entity_name, opset.name, six.text_type(target_type), target_name) return "%s already has %s access to %s %s" % ( entity_name, opset.name, six.text_type(target_type), target_name)
class BofhdExtension(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 EmailCommands(bofhd_email.BofhdEmailCommands): """ UiO specific email commands and overloads. """ all_commands = {} hidden_commands = {} omit_parent_commands = {} parent_commands = True authz = bofhd_auth.EmailAuth @classmethod def get_help_strings(cls): email_cmds = { 'email': { 'email_forward_info': "Show information about an address that is forwarded to", 'email_move': "Move a user's e-mail to another server", 'email_show_reservation_status': "Show reservation status for an account", "email_move_domain_addresses": "Move the first account's e-mail addresses at a domain to " "the second account", } } arg_help = { 'yes_no_move_primary': ['move_primary', 'Should primary email address be moved? (y/n)'], } return merge_help_strings( super(EmailCommands, cls).get_help_strings(), ({}, email_cmds, arg_help)) def __email_forward_destination_allowed(self, account, address): """ Check if the forward is compilant with Norwegian law""" person = Utils.Factory.get('Person')(self.db) if (account.owner_type == self.const.entity_person and person.list_affiliations( person_id=account.owner_id, source_system=self.const.system_sap, affiliation=self.const.affiliation_ansatt)): try: self._get_email_domain_from_str(address.split('@')[-1]) except CerebrumError: return False return True def _get_email_target_and_address(self, address): # Support DistributionGroup email target lookup try: return super(EmailCommands, self)._get_email_target_and_address(address) except CerebrumError as e: # Not found, maybe distribution group? try: dlgroup = Utils.Factory.get("DistributionGroup")(self.db) dlgroup.find_by_name(address) et = Email.EmailTarget(self.db) et.find_by_target_entity(dlgroup.entity_id) epa = Email.EmailPrimaryAddressTarget(self.db) epa.find(et.entity_id) ea = Email.EmailAddress(self.db) ea.find(epa.email_primaddr_id) return et, ea except Errors.NotFoundError: raise e def _get_email_target_and_dlgroup(self, address): """Returns a tuple consisting of the email target associated with address and the account if the target type is user. If there is no at-sign in address, assume it is an account name. Raises CerebrumError if address is unknown.""" et, ea = self._get_email_target_and_address(address) grp = None # what will happen if the target was a dl_group but is now # deleted? it's possible that we should have created a new # target_type = dlgroup_deleted, but it seemed redundant earlier # now, i'm not so sure (Jazz, 2013-12( if et.email_target_type in (self.const.email_target_dl_group, self.const.email_target_deleted): grp = self._get_group(et.email_target_entity_id, idtype='id', grtype="DistributionGroup") return et, grp def _is_email_delivery_stopped(self, ldap_target): """ Test if email delivery is turned off in LDAP for a user. """ import ldap import ldap.filter import ldap.ldapobject ldapconns = [ldap.ldapobject.ReconnectLDAPObject("ldap://%s/" % server) for server in cereconf.LDAP_SERVERS] target_filter = ("(&(target=%s)(mailPause=TRUE))" % ldap.filter.escape_filter_chars(ldap_target)) for conn in ldapconns: try: # FIXME: cereconf.LDAP_MAIL['dn'] has a bogus value, so we # must hardcode the DN. res = conn.search_s("cn=targets,cn=mail,dc=uio,dc=no", ldap.SCOPE_ONELEVEL, target_filter, ["1.1"]) if len(res) != 1: return False except ldap.LDAPError: self.logger.error("LDAP search failed", exc_info=True) return False return True def _email_info_detail(self, acc): info = [] eq = Email.EmailQuota(self.db) try: eq.find_by_target_entity(acc.entity_id) et = Email.EmailTarget(self.db) et.find_by_target_entity(acc.entity_id) es = Email.EmailServer(self.db) es.find(et.email_server_id) # exchange-relatert-jazz # since Exchange-users will have a different kind of # server this code will not be affected at Exchange # roll-out It may, however, be removed as soon as # migration is completed (up to and including # "dis_quota_soft': eq.email_quota_soft})") if es.email_server_type == self.const.email_server_type_cyrus: pw = self.db._read_password(cereconf.CYRUS_HOST, cereconf.CYRUS_ADMIN) used = 'N/A' limit = None try: cyrus = Utils.CerebrumIMAP4_SSL( es.name, ssl_version=ssl.PROTOCOL_TLSv1) # IVR 2007-08-29 If the server is too busy, we do not want # to lock the entire bofhd. # 5 seconds should be enough cyrus.socket().settimeout(5) cyrus.login(cereconf.CYRUS_ADMIN, pw) res, quotas = cyrus.getquota("user." + acc.account_name) cyrus.socket().settimeout(None) if res == "OK": for line in quotas: try: folder, qtype, qused, qlimit = line.split() if qtype == "(STORAGE": used = str(int(qused)/1024) limit = int(qlimit.rstrip(")"))/1024 except ValueError: # line.split fails e.g. because quota isn't set # on server folder, junk = line.split() self.logger.warning("No IMAP quota set for %r", acc.account_name) used = "N/A" limit = None except (bofhd_uio_cmds.TimeoutException, socket.error): used = 'DOWN' except bofhd_uio_cmds.ConnectException as e: used = exc_to_text(e) except imaplib.IMAP4.error: used = 'DOWN' info.append({'quota_hard': eq.email_quota_hard, 'quota_soft': eq.email_quota_soft, 'quota_used': used}) if limit is not None and limit != eq.email_quota_hard: info.append({'quota_server': limit}) else: info.append({'dis_quota_hard': eq.email_quota_hard, 'dis_quota_soft': eq.email_quota_soft}) except Errors.NotFoundError: pass # exchange-relatert-jazz # delivery for exchange-mailboxes is not regulated through # LDAP, and LDAP should not be checked there my be some need # to implement support for checking if delivery is paused in # Exchange, but at this point only very vague explanation has # been given and priority is therefore low if acc.has_spread(self.const.spread_uit_exchange): return info # Check if the ldapservers have set mailPaused if self._is_email_delivery_stopped(acc.account_name): info.append({'status': 'Paused (migrating to new server)'}) return info def _email_info_dlgroup(self, groupname): et, dl_group = self._get_email_target_and_dlgroup(groupname) ret = [] # we need to make the return value conform with the # client requeirements tmpret = dl_group.get_distgroup_attributes_and_targetdata() for x in tmpret: if tmpret[x] == 'T': ret.append({x: 'Yes'}) continue elif tmpret[x] == 'F': ret.append({x: 'No'}) continue ret.append({x: tmpret[x]}) return ret # # email forward_add <account>+ <address>+ # def email_forward_add(self, operator, uname, address): """Add an email-forward to a email-target asociated with an account.""" # Override email_forward_add with check for employee email addr et, acc = self._get_email_target_and_account(uname) if acc and not self.__email_forward_destination_allowed(acc, address): raise CerebrumError("Employees cannot forward e-mail to" " external addresses") return super(EmailCommands, self).email_forward_add(operator, uname, address) # # email forward_info # all_commands['email_forward_info'] = Command( ('email', 'forward_info'), EmailAddress(), fs=FormatSuggestion([('%s', ('id', ))]), perm_filter='can_email_forward_info', ) def email_forward_info(self, operator, forward_to): """List owners of email forwards.""" self.ba.can_email_forward_info(operator.get_entity_id()) ef = Email.EmailForward(self.db) et = Email.EmailTarget(self.db) ac = Utils.Factory.get('Account')(self.db) ret = [] # Different output format for different input. def rfun(r): return (r if '%' not in forward_to else '%-12s %s' % (r, fwd['forward_to'])) for fwd in ef.search(forward_to): try: et.clear() ac.clear() et.find(fwd['target_id']) ac.find(et.email_target_entity_id) ret.append({'id': rfun(ac.account_name)}) except Errors.NotFoundError: ret.append({'id': rfun('id:%s' % et.entity_id)}) return ret # # email show_reservation_status # all_commands['email_show_reservation_status'] = Command( ('email', 'show_reservation_status'), AccountName(), fs=FormatSuggestion([("%-9s %s", ("uname", "hide"))]), perm_filter='is_postmaster') def email_show_reservation_status(self, operator, uname): """Display reservation status for a person.""" if not self.ba.is_postmaster(operator.get_entity_id()): raise PermissionDenied('Access to this command is restricted') hidden = True account = self._get_account(uname) if account.owner_type == self.const.entity_person: person = self._get_person('entity_id', account.owner_id) if person.has_e_reservation(): hidden = True elif person.get_primary_account() != account.entity_id: hidden = True else: hidden = False return { 'uname': uname, 'hide': 'hidden' if hidden else 'visible', } # # email move # all_commands['email_move'] = Command( ("email", "move"), AccountName(help_ref="account_name", repeat=True), SimpleString(help_ref='string_email_host'), perm_filter='can_email_move') def email_move(self, operator, uname, server): acc = self._get_account(uname) self.ba.can_email_move(operator.get_entity_id(), acc) et = Email.EmailTarget(self.db) et.find_by_target_entity(acc.entity_id) old_server = et.email_server_id es = Email.EmailServer(self.db) try: es.find_by_name(server) except Errors.NotFoundError: raise CerebrumError("%r is not registered as an e-mail server" % server) if old_server == es.entity_id: raise CerebrumError("User is already at %s" % server) et.email_server_id = es.entity_id et.write_db() return "OK, updated e-mail server for %s (to %s)" % (uname, server) # # email move_domain_addresses # all_commands['email_move_domain_addresses'] = Command( ("email", "move_domain_addresses"), AccountName(help_ref="account_name"), AccountName(help_ref="account_name"), SimpleString(help_ref='email_domain', optional=True, default=cereconf.NO_MAILBOX_DOMAIN_EMPLOYEES), YesNo(help_ref='yes_no_move_primary', optional=True, default="No"), perm_filter="is_superuser") def _move_email_address(self, address, reassigned_addresses, dest_et): ea = Email.EmailAddress(self.db) ea.find(address['address_id']) ea.email_addr_target_id = dest_et.entity_id ea.write_db() reassigned_addresses.append(ea.get_address()) def _move_primary_email_address(self, address, reassigned_addresses, dest_et, epat): epat.delete() self._move_email_address(address, reassigned_addresses, dest_et) epat.clear() try: epat.find(dest_et.entity_id) except Errors.NotFoundError: pass else: epat.delete() epat.clear() epat.populate(address['address_id'], parent=dest_et) epat.write_db() def _move_ad_email(self, email, dest_uname): ad = ad_email.AdEmail(self.db) ad.delete_ad_email(account_name=dest_uname) ad.set_ad_email(dest_uname, email['local_part'], email['domain_part']) ad_emails_added = "Updated ad email {} for {}. ".format( email['local_part']+"@"+email['domain_part'], dest_uname ) return ad_emails_added def email_move_domain_addresses(self, operator, source_uname, dest_uname, domain_str, move_primary): """Move an account's e-mail addresses to another account :param domain_str: email domain to be affected :param move_primary: move primary email address """ if not self.ba.is_superuser(operator.get_entity_id()): raise PermissionDenied("Currently limited to superusers") move_primary = self._get_boolean(move_primary) source_account = self._get_account(source_uname) source_et = self._get_email_target_for_account(source_account) dest_account = self._get_account(dest_uname) dest_et = self._get_email_target_for_account(dest_account) epat = Email.EmailPrimaryAddressTarget(self.db) try: epat.find(source_et.entity_id) except Errors.NotFoundError: epat.clear() reassigned_addresses = [] for address in source_et.get_addresses(): if address['domain'] == domain_str: if address['address_id'] == epat.email_primaddr_id: if move_primary: self._move_primary_email_address(address, reassigned_addresses, dest_et, epat) else: self._move_email_address(address, reassigned_addresses, dest_et) # Managing ad_email ad_emails_added = "" if domain_str == cereconf.NO_MAILBOX_DOMAIN_EMPLOYEES: ad = ad_email.AdEmail(self.db) if move_primary: ad_emails = ad.search_ad_email(account_name=source_uname) if len(ad_emails) == 1: ad_emails_added = self._move_ad_email(ad_emails[0], dest_uname) # TODO: # If this command is called with move_primary=False, # the source account's primary email address will be left # intact, but it's corresponding ad_email will be deleted. # This mimics the functionality of the uit-script move_emails.py, # but is it really what we want? ad.delete_ad_email(account_name=source_uname) return ("OK, reassigned {}. ".format(reassigned_addresses) + ad_emails_added)
class BofhdExtension(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(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(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 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): """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 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