예제 #1
0
    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
예제 #2
0
    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)
예제 #3
0
 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
예제 #4
0
파일: parser.py 프로젝트: icing/irrd
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
예제 #5
0
    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))
예제 #6
0
    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
예제 #7
0
 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
예제 #8
0
    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'}, ), {}],
        ]
예제 #9
0
파일: parser.py 프로젝트: icing/irrd
 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