def notification_target_report(self): """ Produce a string suitable for reporting back status and messages to a human notification target, i.e. someone listed in notify/upd-to/mnt-nfy. """ if not self.is_valid( ) and self.status != UpdateRequestStatus.ERROR_AUTH: raise ValueError( 'Notification reports can only be made for changes that are valid ' 'or have failed authorisation.') status = 'succeeded' if self.is_valid() else 'FAILED AUTHORISATION' report = f'{self.request_type_str().title()} {status} for object below: ' report += f'[{self.object_class_str()}] {self.object_pk_str()}:\n\n' if self.request_type == UpdateRequestType.MODIFY: current_text = list( splitline_unicodesafe( self.rpsl_obj_current.render_rpsl_text())) new_text = list( splitline_unicodesafe(self.rpsl_obj_new.render_rpsl_text())) diff = list( difflib.unified_diff(current_text, new_text, lineterm='')) report += '\n'.join( diff[2:] ) # skip the lines from the diff which would have filenames if self.status == UpdateRequestStatus.ERROR_AUTH: report += '\n\n*Rejected* new version of this object:\n\n' else: report += '\n\nNew version of this object:\n\n' report += self.rpsl_obj_new.render_rpsl_text() return report
def _normalise_rpsl_value(self, value: str) -> str: """ Normalise an RPSL attribute value to its significant parts in a consistent format. For example, the following is valid in RPSL: inetnum: 192.0.2.0 # comment1 +- # comment 2 +192.0.2.1 # comment 3 + # comment 4 This value will be normalised by this method to: 192.0.2.0 - 192.0.2.1 to be used for further validation and extraction of primary keys. """ normalized_lines = [] # The shortcuts below are functionally inconsequential, but significantly improve performance, # as most values are single line without comments, and this method is called extremely often. if '\n' not in value: if '#' in value: return value.split('#')[0].strip() return value.strip() for line in splitline_unicodesafe(value): parsed_line = line.split('#')[0].strip('\n\t, ') if parsed_line: normalized_lines.append(parsed_line) return ','.join(normalized_lines)
def render_rpsl_text(self, last_modified: datetime.datetime = None) -> str: """ Render the RPSL object as an RPSL string. If last_modified is provided, removes existing last-modified: attributes and adds a new one with that timestamp, if self.source() is authoritative. """ output = "" authoritative = get_setting(f'sources.{self.source()}.authoritative') for attr, value, continuation_chars in self._object_data: if authoritative and last_modified and attr == 'last-modified': continue attr_display = f'{attr}:'.ljust(RPSL_ATTRIBUTE_TEXT_WIDTH) value_lines = list(splitline_unicodesafe(value)) if not value_lines: output += f'{attr}:\n' for idx, line in enumerate(value_lines): if idx == 0: output += attr_display + line else: continuation_char = continuation_chars[idx - 1] # Override the continuation char for empty lines #298 if not line: continuation_char = '+' output += continuation_char + (RPSL_ATTRIBUTE_TEXT_WIDTH - 1) * ' ' + line output += '\n' if authoritative and last_modified: output += 'last-modified:'.ljust(RPSL_ATTRIBUTE_TEXT_WIDTH) output += last_modified.replace(microsecond=0).isoformat().replace( '+00:00', 'Z') output += '\n' return output
def parse_change_requests( requests_text: str, database_handler: DatabaseHandler, auth_validator: AuthValidator, reference_validator: ReferenceValidator, ) -> List[ChangeRequest]: """ Parse change requests, a text of RPSL objects along with metadata like passwords or deletion requests. :param requests_text: a string containing all change requests :param database_handler: a DatabaseHandler instance :param auth_validator: a AuthValidator instance, to resolve authentication requirements :param reference_validator: a ReferenceValidator instance :return: a list of ChangeRequest instances """ results = [] passwords = [] overrides = [] requests_text = requests_text.replace('\r', '') for object_text in requests_text.split('\n\n'): object_text = object_text.strip() if not object_text: continue rpsl_text = '' delete_reason = None # The attributes password/override/delete are meta attributes # and need to be extracted before parsing. Delete refers to a specific # object, password/override apply to all included objects. for line in splitline_unicodesafe(object_text): if line.startswith('password:'******':', maxsplit=1)[1].strip() passwords.append(password) elif line.startswith('override:'): override = line.split(':', maxsplit=1)[1].strip() overrides.append(override) elif line.startswith('delete:'): delete_reason = line.split(':', maxsplit=1)[1].strip() else: rpsl_text += line + '\n' if not rpsl_text: continue results.append( ChangeRequest(rpsl_text, database_handler, auth_validator, reference_validator, delete_reason=delete_reason)) if auth_validator: auth_validator.passwords = passwords auth_validator.overrides = overrides return results
def _extract_attributes_values(self, text: str) -> None: """ Extract all attributes and associated values from the input string. This is mostly straight forward, except for the tricky feature of line continuation. An attribute's value can be continued on the next lines, which is distinct from an attribute occurring multiple times. The parse result is internally stored in self._object_data. This is a list of 3-tuples, where each tuple contains the attribute name, attribute value, and the continuation characters. The continuation characters are needed to reconstruct the original object into a string. """ continuation_chars = (' ', '+', '\t') current_attr = None current_value = "" current_continuation_chars: List[str] = [] for line_no, line in enumerate(splitline_unicodesafe(text.strip())): if not line: self.messages.error( f'Line {line_no+1}: encountered empty line in the middle of object: [{line}]' ) return if not line.startswith(continuation_chars): if current_attr and current_attr not in self.discarded_fields: # Encountering a new attribute requires saving the previous attribute data first, if any, # which can't be done earlier as line continuation means we can't know earlier whether # the attribute is finished. self._object_data.append((current_attr, current_value, current_continuation_chars)) if ':' not in line: self.messages.error( f'Line {line_no+1}: line is neither continuation nor valid attribute [{line}]' ) return current_attr, current_value = line.split(':', maxsplit=1) current_attr = current_attr.lower() current_value = current_value.strip() current_continuation_chars = [] if current_attr not in self.attrs_allowed and not self._re_attr_name.match( current_attr): self.messages.error( f'Line {line_no+1}: encountered malformed attribute name: [{current_attr}]' ) return else: # Whitespace between the continuation character and the start of the data is not significant. current_value += '\n' + line[1:].strip() current_continuation_chars += line[0] if current_attr and current_attr not in self.discarded_fields: self._object_data.append( (current_attr, current_value, current_continuation_chars))
def render_diff(self, query: str, cleaned_reference: str, cleaned_tested: str) -> Optional[str]: """Produce a diff between the results, either by line or with queries like !i, by element returned.""" if not cleaned_reference or not cleaned_tested: return None irr_query = query[:2].lower() if irr_query in SSP_QUERIES or (irr_query == '!r' and query.lower().strip().endswith(',o')): diff_input_reference = list(cleaned_reference.split(' ')) diff_input_tested = list(cleaned_tested.split(' ')) else: diff_input_reference = list( splitline_unicodesafe(cleaned_reference)) diff_input_tested = list(splitline_unicodesafe(cleaned_tested)) diff = list( difflib.unified_diff(diff_input_reference, diff_input_tested, lineterm='')) diff_str = '\n'.join( diff[2:] ) # skip the lines from the diff which would have filenames return diff_str
def render_rpsl_text(self) -> str: """Render the RPSL object as an RPSL string.""" output = "" for attr, value, continuation_chars in self._object_data: attr_display = f"{attr}:".ljust(RPSL_ATTRIBUTE_TEXT_WIDTH) value_lines = list(splitline_unicodesafe(value)) if not value_lines: output += f"{attr}:\n" for idx, line in enumerate(value_lines): if idx == 0: output += attr_display + line else: output += continuation_chars[idx-1] + (RPSL_ATTRIBUTE_TEXT_WIDTH-1) * " " + line output += "\n" return output
def test_check_auth_valid_update_mntner_submits_new_object_with_all_dummy_hash_values( self, prepare_mocks): mock_dq, mock_dh = prepare_mocks mock_dh.execute_query = lambda query: [{'object_text': SAMPLE_MNTNER}] reference_validator = ReferenceValidator(mock_dh) auth_validator = AuthValidator(mock_dh) # Submit the mntner with dummy password values as would be returned by queries, # but a password attribute that is valid for the current DB object. data = SAMPLE_MNTNER.replace('LEuuhsBJNFV0Q', PASSWORD_HASH_DUMMY_VALUE) data = data.replace('$1$fgW84Y9r$kKEn9MUq8PChNKpQhO6BM.', PASSWORD_HASH_DUMMY_VALUE) result_mntner = parse_change_requests( data + 'password: crypt-password', mock_dh, auth_validator, reference_validator)[0] auth_validator.pre_approve([result_mntner]) assert result_mntner._check_auth() assert not result_mntner.error_messages assert result_mntner.info_messages == [ 'As you submitted dummy hash values, all password hashes on this object ' 'were replaced with a new MD5-PW hash of the password you provided for ' 'authentication.' ] auth_pgp, auth_hash = splitline_unicodesafe( result_mntner.rpsl_obj_new.parsed_data['auth']) assert auth_pgp == 'PGPKey-80F238C6' assert auth_hash.startswith('MD5-PW ') assert md5_crypt.verify('crypt-password', auth_hash[7:]) assert auth_hash in result_mntner.rpsl_obj_new.render_rpsl_text() assert flatten_mock_calls(mock_dq) == [ ['sources', (['TEST'], ), {}], ['object_classes', (['mntner'], ), {}], ['rpsl_pk', ('TEST-MNT', ), {}], ['sources', (['TEST'], ), {}], ['object_classes', (['mntner'], ), {}], ['rpsl_pks', ({'OTHER1-MNT', 'OTHER2-MNT', 'TEST-MNT'}, ), {}], ['sources', (['TEST'], ), {}], ['object_classes', (['mntner'], ), {}], ['rpsl_pks', ({'OTHER1-MNT', 'OTHER2-MNT'}, ), {}], ]
def render_rpsl_text(self) -> str: """Render the RPSL object as an RPSL string.""" output = "" for attr, value, continuation_chars in self._object_data: attr_display = f'{attr}:'.ljust(RPSL_ATTRIBUTE_TEXT_WIDTH) value_lines = list(splitline_unicodesafe(value)) if not value_lines: output += f'{attr}:\n' for idx, line in enumerate(value_lines): if idx == 0: output += attr_display + line else: continuation_char = continuation_chars[idx - 1] # Override the continuation char for empty lines #298 if not line: continuation_char = '+' output += continuation_char + (RPSL_ATTRIBUTE_TEXT_WIDTH - 1) * ' ' + line output += '\n' return output