예제 #1
0
파일: parser.py 프로젝트: icing/irrd
    def __init__(self,
                 rpsl_text_submitted: str,
                 database_handler: DatabaseHandler,
                 auth_validator: AuthValidator,
                 reference_validator: ReferenceValidator,
                 delete_reason=Optional[str]) -> None:
        """
        Initialise a new change request for a single RPSL object.

        :param rpsl_text_submitted: the object text
        :param database_handler: a DatabaseHandler instance
        :param auth_validator: a AuthValidator instance, to resolve authentication requirements
        :param reference_validator: a ReferenceValidator instance, to resolve references between objects
        :param delete_reason: a string with the deletion reason, if this was a deletion request

        The rpsl_text passed into this function should be cleaned from any
        meta attributes like delete/override/password. Those should be passed
        into this method as delete_reason, or provided to the AuthValidator.

        The auth_validator and reference_validator must be shared between
        different instances, to benefit from caching, and to resolve references
        between different objects that are part of the same submission with
        possibly multiple changes.
        """
        self.database_handler = database_handler
        self.auth_validator = auth_validator
        self.reference_validator = reference_validator
        self.rpsl_text_submitted = rpsl_text_submitted
        self.mntners_notify = []
        self.used_override = False
        self._cached_roa_validity: Optional[bool] = None
        self.roa_validator = SingleRouteROAValidator(database_handler)

        try:
            self.rpsl_obj_new = rpsl_object_from_text(rpsl_text_submitted,
                                                      strict_validation=True)
            if self.rpsl_obj_new.messages.errors():
                self.status = UpdateRequestStatus.ERROR_PARSING
            self.error_messages = self.rpsl_obj_new.messages.errors()
            self.info_messages = self.rpsl_obj_new.messages.infos()
            logger.debug(
                f'{id(self)}: Processing new ChangeRequest for object {self.rpsl_obj_new}: request {id(self)}'
            )

        except UnknownRPSLObjectClassException as exc:
            self.rpsl_obj_new = None
            self.request_type = None
            self.status = UpdateRequestStatus.ERROR_UNKNOWN_CLASS
            self.info_messages = []
            self.error_messages = [str(exc)]

        if self.is_valid() and self.rpsl_obj_new:
            source = self.rpsl_obj_new.source()
            if not get_setting(f'sources.{source}.authoritative'):
                logger.debug(
                    f'{id(self)}: change is for non-authoritative source {source}, rejected'
                )
                self.error_messages.append(
                    f'This instance is not authoritative for source {source}')
                self.status = UpdateRequestStatus.ERROR_NON_AUTHORITIVE
                return

            self._retrieve_existing_version()

        if delete_reason:
            self.request_type = UpdateRequestType.DELETE
            if not self.rpsl_obj_current:
                self.status = UpdateRequestStatus.ERROR_PARSING
                self.error_messages.append(
                    'Can not delete object: no object found for this key in this database.'
                )
                logger.debug(
                    f'{id(self)}: Request attempts to delete object {self.rpsl_obj_new}, '
                    f'but no existing object found.')
예제 #2
0
    def save(self, database_handler: DatabaseHandler) -> bool:
        default_source = self.source if self.operation == DatabaseOperation.delete else None
        try:
            object_text = self.object_text.strip()
            # If an object turns out to be a key-cert, and strict_import_keycert_objects
            # is set, parse it again with strict validation to load it in the GPG keychain.
            obj = rpsl_object_from_text(object_text,
                                        strict_validation=False,
                                        default_source=default_source)
            if self.strict_validation_key_cert and obj.__class__ == RPSLKeyCert:
                obj = rpsl_object_from_text(object_text,
                                            strict_validation=True,
                                            default_source=default_source)

        except UnknownRPSLObjectClassException as exc:
            # Unknown object classes are only logged if they have not been filtered out.
            if not self.object_class_filter or exc.rpsl_object_class.lower(
            ) in self.object_class_filter:
                logger.info(f'Ignoring NRTM operation {str(self)}: {exc}')
            return False

        if self.object_class_filter and obj.rpsl_object_class.lower(
        ) not in self.object_class_filter:
            return False

        if obj.messages.errors():
            errors = '; '.join(obj.messages.errors())
            logger.critical(
                f'Parsing errors occurred while processing NRTM operation {str(self)}. '
                f'This operation is ignored, causing potential data inconsistencies. '
                f'A new operation for this update, without errors, '
                f'will still be processed and cause the inconsistency to be resolved. '
                f'Parser error messages: {errors}; original object text follows:\n{self.object_text}'
            )
            database_handler.record_mirror_error(
                self.source, f'Parsing errors: {obj.messages.errors()}, '
                f'original object text follows:\n{self.object_text}')
            return False

        if 'source' in obj.parsed_data and obj.parsed_data['source'].upper(
        ) != self.source:
            msg = (
                f'Incorrect source in NRTM object: stream has source {self.source}, found object with '
                f'source {obj.source()} in operation {self.serial}/{self.operation.value}/{obj.pk()}. '
                f'This operation is ignored, causing potential data inconsistencies.'
            )
            database_handler.record_mirror_error(self.source, msg)
            logger.critical(msg)
            return False

        if self.operation == DatabaseOperation.add_or_update:
            if self.rpki_aware and obj.rpki_relevant and obj.prefix and obj.asn_first:
                roa_validator = SingleRouteROAValidator(database_handler)
                obj.rpki_status = roa_validator.validate_route(
                    obj.prefix, obj.asn_first, obj.source())
            scope_validator = ScopeFilterValidator()
            obj.scopefilter_status, _ = scope_validator.validate_rpsl_object(
                obj)
            database_handler.upsert_rpsl_object(obj,
                                                JournalEntryOrigin.mirror,
                                                source_serial=self.serial)
        elif self.operation == DatabaseOperation.delete:
            database_handler.delete_rpsl_object(
                rpsl_object=obj,
                origin=JournalEntryOrigin.mirror,
                source_serial=self.serial)

        log = f'Completed NRTM operation {str(self)}/{obj.rpsl_object_class}/{obj.pk()}'
        if self.rpki_aware and obj.rpki_relevant:
            log += f', RPKI status {obj.rpki_status.value}'
        logger.info(log)
        return True
예제 #3
0
파일: parser.py 프로젝트: icing/irrd
class ChangeRequest:
    """
    A ChangeRequest tracks and processes a request for a single change.
    In this context, a change can be creating, modifying or deleting an
    RPSL object.
    """
    rpsl_text_submitted: str
    rpsl_obj_new: Optional[RPSLObject]
    rpsl_obj_current: Optional[RPSLObject] = None
    status = UpdateRequestStatus.PROCESSING
    request_type: Optional[UpdateRequestType] = None
    mntners_notify: List[RPSLMntner]

    error_messages: List[str]
    info_messages: List[str]

    def __init__(self,
                 rpsl_text_submitted: str,
                 database_handler: DatabaseHandler,
                 auth_validator: AuthValidator,
                 reference_validator: ReferenceValidator,
                 delete_reason=Optional[str]) -> None:
        """
        Initialise a new change request for a single RPSL object.

        :param rpsl_text_submitted: the object text
        :param database_handler: a DatabaseHandler instance
        :param auth_validator: a AuthValidator instance, to resolve authentication requirements
        :param reference_validator: a ReferenceValidator instance, to resolve references between objects
        :param delete_reason: a string with the deletion reason, if this was a deletion request

        The rpsl_text passed into this function should be cleaned from any
        meta attributes like delete/override/password. Those should be passed
        into this method as delete_reason, or provided to the AuthValidator.

        The auth_validator and reference_validator must be shared between
        different instances, to benefit from caching, and to resolve references
        between different objects that are part of the same submission with
        possibly multiple changes.
        """
        self.database_handler = database_handler
        self.auth_validator = auth_validator
        self.reference_validator = reference_validator
        self.rpsl_text_submitted = rpsl_text_submitted
        self.mntners_notify = []
        self.used_override = False
        self._cached_roa_validity: Optional[bool] = None
        self.roa_validator = SingleRouteROAValidator(database_handler)

        try:
            self.rpsl_obj_new = rpsl_object_from_text(rpsl_text_submitted,
                                                      strict_validation=True)
            if self.rpsl_obj_new.messages.errors():
                self.status = UpdateRequestStatus.ERROR_PARSING
            self.error_messages = self.rpsl_obj_new.messages.errors()
            self.info_messages = self.rpsl_obj_new.messages.infos()
            logger.debug(
                f'{id(self)}: Processing new ChangeRequest for object {self.rpsl_obj_new}: request {id(self)}'
            )

        except UnknownRPSLObjectClassException as exc:
            self.rpsl_obj_new = None
            self.request_type = None
            self.status = UpdateRequestStatus.ERROR_UNKNOWN_CLASS
            self.info_messages = []
            self.error_messages = [str(exc)]

        if self.is_valid() and self.rpsl_obj_new:
            source = self.rpsl_obj_new.source()
            if not get_setting(f'sources.{source}.authoritative'):
                logger.debug(
                    f'{id(self)}: change is for non-authoritative source {source}, rejected'
                )
                self.error_messages.append(
                    f'This instance is not authoritative for source {source}')
                self.status = UpdateRequestStatus.ERROR_NON_AUTHORITIVE
                return

            self._retrieve_existing_version()

        if delete_reason:
            self.request_type = UpdateRequestType.DELETE
            if not self.rpsl_obj_current:
                self.status = UpdateRequestStatus.ERROR_PARSING
                self.error_messages.append(
                    'Can not delete object: no object found for this key in this database.'
                )
                logger.debug(
                    f'{id(self)}: Request attempts to delete object {self.rpsl_obj_new}, '
                    f'but no existing object found.')

    def _retrieve_existing_version(self):
        """
        Retrieve the current version of this object, if any, and store it in rpsl_obj_current.
        Update self.status appropriately.
        """
        query = RPSLDatabaseQuery().sources([self.rpsl_obj_new.source()])
        query = query.object_classes([self.rpsl_obj_new.rpsl_object_class
                                      ]).rpsl_pk(self.rpsl_obj_new.pk())
        results = list(self.database_handler.execute_query(query))

        if not results:
            self.request_type = UpdateRequestType.CREATE
            logger.debug(
                f'{id(self)}: Did not find existing version for object {self.rpsl_obj_new}, request is CREATE'
            )
        elif len(results) == 1:
            self.request_type = UpdateRequestType.MODIFY
            self.rpsl_obj_current = rpsl_object_from_text(
                results[0]['object_text'], strict_validation=False)
            logger.debug(f'{id(self)}: Retrieved existing version for object '
                         f'{self.rpsl_obj_current}, request is MODIFY/DELETE')
        else:  # pragma: no cover
            # This should not be possible, as rpsl_pk/source are a composite unique value in the database scheme.
            # Therefore, a query should not be able to affect more than one row.
            affected_pks = ', '.join([r['pk'] for r in results])
            msg = f'{id(self)}: Attempted to retrieve current version of object {self.rpsl_obj_new.pk()}/'
            msg += f'{self.rpsl_obj_new.source()}, but multiple '
            msg += f'objects were found, internal pks found: {affected_pks}'
            logger.error(msg)
            raise ValueError(msg)

    def save(self, database_handler: DatabaseHandler) -> None:
        """Save the change to the database."""
        if self.status != UpdateRequestStatus.PROCESSING or not self.rpsl_obj_new:
            raise ValueError(
                'ChangeRequest can only be saved in status PROCESSING')
        if self.request_type == UpdateRequestType.DELETE and self.rpsl_obj_current is not None:
            logger.info(
                f'{id(self)}: Saving change for {self.rpsl_obj_new}: deleting current object'
            )
            database_handler.delete_rpsl_object(
                rpsl_object=self.rpsl_obj_current,
                origin=JournalEntryOrigin.auth_change)
        else:
            if not self.used_override:
                self.rpsl_obj_new.overwrite_date_new_changed_attributes(
                    self.rpsl_obj_current)
                # This call may have emitted a new info message.
                self._import_new_rpsl_obj_info_messages()
            logger.info(
                f'{id(self)}: Saving change for {self.rpsl_obj_new}: inserting/updating current object'
            )
            database_handler.upsert_rpsl_object(self.rpsl_obj_new,
                                                JournalEntryOrigin.auth_change)
        self.status = UpdateRequestStatus.SAVED

    def is_valid(self):
        return self.status in [
            UpdateRequestStatus.SAVED, UpdateRequestStatus.PROCESSING
        ]

    def submitter_report(self) -> str:
        """Produce a string suitable for reporting back status and messages to the human submitter."""
        status = 'succeeded' if self.is_valid() else 'FAILED'

        report = f'{self.request_type_str().title()} {status}: [{self.object_class_str()}] {self.object_pk_str()}\n'
        if self.info_messages or self.error_messages:
            if not self.rpsl_obj_new or self.error_messages:
                report += '\n' + self.rpsl_text_submitted + '\n'
            else:
                report += '\n' + self.rpsl_obj_new.render_rpsl_text() + '\n'
            report += ''.join([f'ERROR: {e}\n' for e in self.error_messages])
            report += ''.join([f'INFO: {e}\n' for e in self.info_messages])
        return report

    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'

        if self.request_type == UpdateRequestType.DELETE:
            report += self.rpsl_obj_current.render_rpsl_text()
        else:
            report += self.rpsl_obj_new.render_rpsl_text()
        return report

    def request_type_str(self) -> str:
        return self.request_type.value if self.request_type else 'request'

    def object_pk_str(self) -> str:
        return self.rpsl_obj_new.pk(
        ) if self.rpsl_obj_new else '(unreadable object key)'

    def object_class_str(self) -> str:
        return self.rpsl_obj_new.rpsl_object_class if self.rpsl_obj_new else '(unreadable object class)'

    def notification_targets(self) -> Set[str]:
        """
        Produce a set of e-mail addresses that should be notified
        about the change to this object.
        May include mntner upd-to or mnt-nfy, and notify of existing object.
        """
        targets: Set[str] = set()
        status_qualifies_notification = self.is_valid(
        ) or self.status == UpdateRequestStatus.ERROR_AUTH
        if self.used_override or not status_qualifies_notification:
            return targets

        mntner_attr = 'upd-to' if self.status == UpdateRequestStatus.ERROR_AUTH else 'mnt-nfy'
        for mntner in self.mntners_notify:
            for email in mntner.parsed_data.get(mntner_attr, []):
                targets.add(email)

        if self.rpsl_obj_current:
            for email in self.rpsl_obj_current.parsed_data.get('notify', []):
                targets.add(email)

        return targets

    def validate(self) -> bool:
        auth_valid = self._check_auth()
        if not auth_valid:
            return False
        references_valid = self._check_references()
        self.notification_targets()
        rpki_valid = self._check_conflicting_roa()
        return references_valid and rpki_valid

    def _check_auth(self) -> bool:
        assert self.rpsl_obj_new
        auth_result = self.auth_validator.process_auth(self.rpsl_obj_new,
                                                       self.rpsl_obj_current)
        self.info_messages += auth_result.info_messages
        self.mntners_notify = auth_result.mntners_notify

        if not auth_result.is_valid():
            self.status = UpdateRequestStatus.ERROR_AUTH
            self.error_messages += auth_result.error_messages
            logger.debug(
                f'{id(self)}: Authentication check failed: {auth_result.error_messages}'
            )
            return False

        self.used_override = auth_result.used_override

        logger.debug(f'{id(self)}: Authentication check succeeded')
        return True

    def _check_references(self) -> bool:
        """
        Check all references from this object to or from other objects.

        For deletions, only references to the deleted object matter, as
        they now become invalid. For other operations, only the validity
        of references from the new object to others matter.
        """
        if self.request_type == UpdateRequestType.DELETE and self.rpsl_obj_current is not None:
            assert self.rpsl_obj_new
            references_result = self.reference_validator.check_references_from_others(
                self.rpsl_obj_current)
        else:
            assert self.rpsl_obj_new
            references_result = self.reference_validator.check_references_to_others(
                self.rpsl_obj_new)
        self.info_messages += references_result.info_messages

        if not references_result.is_valid():
            self.error_messages += references_result.error_messages
            logger.debug(
                f'{id(self)}: Reference check failed: {references_result.error_messages}'
            )
            if self.is_valid(
            ):  # Only change the status if this object was valid prior, so this is the first failure
                self.status = UpdateRequestStatus.ERROR_REFERENCE
            return False

        logger.debug(f'{id(self)}: Reference check succeeded')
        return True

    def _check_conflicting_roa(self) -> bool:
        """
        Check whether there are any conflicting ROAs with the new object.
        Result is cached, as validate() may be called multiple times,
        but the result of this check will not change. Always returns
        True when not in RPKI-aware mode.
        """
        assert self.rpsl_obj_new
        if self._cached_roa_validity is not None:
            return self._cached_roa_validity
        if not get_setting(
                'rpki.roa_source') or not self.rpsl_obj_new.rpki_relevant:
            return True
        # Deletes are permitted for RPKI-invalids, other operations are not
        if self.request_type == UpdateRequestType.DELETE:
            return True

        assert self.rpsl_obj_new.asn_first
        validation_result = self.roa_validator.validate_route(
            self.rpsl_obj_new.prefix, self.rpsl_obj_new.asn_first,
            self.rpsl_obj_new.source())
        if validation_result == RPKIStatus.invalid:
            import_timer = get_setting('rpki.roa_import_timer')
            user_message = 'RPKI ROAs were found that conflict with this object. '
            user_message += f'(This IRRd refreshes ROAs every {import_timer} seconds.)'
            logger.debug(f'{id(self)}: Conflicting ROAs found')
            if self.is_valid(
            ):  # Only change the status if this object was valid prior, so this is first failure
                self.status = UpdateRequestStatus.ERROR_ROA
            self.error_messages.append(user_message)
            self._cached_roa_validity = False
            return False
        else:
            logger.debug(f'{id(self)}: No conflicting ROAs found')
        self._cached_roa_validity = True
        return True

    def _import_new_rpsl_obj_info_messages(self):
        """
        Import new info messages from self.rpsl_obj_new.
        This is used after overwrite_date_new_changed_attributes()
        is called, as it's called just before saving, but may
        emit a new info message.
        """
        for info_message in self.rpsl_obj_new.messages.infos():
            if info_message not in self.info_messages:
                self.info_messages.append(info_message)
예제 #4
0
def reactivate_for_mntner(
        database_handler: DatabaseHandler,
        reactivated_mntner: RPSLMntner) -> Tuple[List[RPSLObject], List[str]]:
    """
    Reactivate previously suspended mntners and return the restored objects.

    Revives objects that were previously suspended with suspend_for_mntner.
    All RPSL objects that had `reactivated_mntner` as one of their mnt-by's at
    the time of suspension are restored. Note that this is potentially different
    from "all objects that were suspended at the time `reactivated_mntner` was
    suspended". Reactivated objects are removed from the suspended store.

    If an object is to be reactivated, but there is already another RPSL object
    with the same class and primary key in the same source, the reactivation
    is skipped. The object remains in the suspended store.

    Throws a ValueError if not authoritative for this source or reactivated_mntner
    does not exist in the suspended store.
    Returns a tuple of all reactivated RPSL objects and a list
    of info messages about reactivated and skipped objects.
    """
    log_prelude = f'reactivation {reactivated_mntner.pk()}'
    source = reactivated_mntner.source()
    scopefilter_validator = ScopeFilterValidator()
    roa_validator = SingleRouteROAValidator(database_handler)

    if not get_setting(f'sources.{source}.authoritative'):
        raise ValueError(f'Not authoritative for source {source}')

    logger.info(
        f"{log_prelude}: Starting reactivation for for {reactivated_mntner}")

    def pk_exists(pk: str, rpsl_object_class: str) -> bool:
        existing_object_query = RPSLDatabaseQuery(column_names=['pk']).sources(
            [source])
        existing_object_query = existing_object_query.rpsl_pk(
            pk).object_classes([rpsl_object_class])
        return bool(list(
            database_handler.execute_query(existing_object_query)))

    if pk_exists(reactivated_mntner.pk(), 'mntner'):
        msg = f"source {source} has a currently active mntner {reactivated_mntner.pk()} - can not restore the suspended one"
        logger.info(f"{log_prelude}: error: {msg}")
        raise ValueError(msg)

    # This is both a check to make sure the mntner is actually suspended,
    # but also to catch cases where a suspended mntner did not have itself as mnt-by.
    query = RPSLDatabaseSuspendedQuery().sources([source]).rpsl_pk(
        reactivated_mntner.pk()).object_classes(['mntner'])
    results = list(database_handler.execute_query(query))

    if not results:
        msg = f"mntner {reactivated_mntner.pk()} not found in suspended store for source {source}"
        logger.info(f"{log_prelude}: error: {msg}")
        raise ValueError(msg)

    query = RPSLDatabaseSuspendedQuery().sources([source]).mntner(
        reactivated_mntner.pk())
    results += list(database_handler.execute_query(query))

    restored_row_pk_uuids = set()
    restored_objects = []
    info_messages: List[str] = []

    for result in results:
        if result['pk'] in restored_row_pk_uuids:
            continue

        reactivating_obj = rpsl_object_from_text(result['object_text'],
                                                 strict_validation=False)

        if pk_exists(reactivating_obj.pk(),
                     reactivating_obj.rpsl_object_class):
            msg = f"Skipping restore of object {reactivating_obj} - an object already exists with the same key"
            logger.info(f"{log_prelude}: {msg}")
            info_messages.append(msg)
            continue

        reactivating_obj.scopefilter_status, _ = scopefilter_validator.validate_rpsl_object(
            reactivating_obj)
        if get_setting(
                'rpki.roa_source'
        ) and reactivating_obj.rpki_relevant and reactivating_obj.asn_first:
            reactivating_obj.rpki_status = roa_validator.validate_route(
                reactivating_obj.prefix, reactivating_obj.asn_first, source)

        database_handler.upsert_rpsl_object(
            reactivating_obj,
            JournalEntryOrigin.suspension,
            forced_created_value=result['original_created'])
        restored_row_pk_uuids.add(result['pk'])
        restored_objects.append(reactivating_obj)
        logger.info(f"{log_prelude}: Restoring object {reactivating_obj}")

    database_handler.delete_suspended_rpsl_objects(restored_row_pk_uuids)
    return restored_objects, info_messages