예제 #1
0
    def fetch_token(self, code, state):
        """
        Provided a code and a state (provided by the client once redirected
        back to the login callback URL), fetches a token from Hydra.
        The token contains ID token, Access token, Refresh token,
        among other things.

        Use self.get_id_token() to decode the ID token.

        :param str code:
        :param str state:
        :rtype: collections.abc.Mapping
        """
        try:
            return self.client.fetch_token(
                url=HYDRA_TOKEN_ENDPOINT,
                grant_type='authorization_code',
                code=code,
                state=state,
                redirect_uri=LOGIN_CALLBACK_URL,
                verify=not DEBUG,
            )
        except json.decoder.JSONDecodeError as e:
            logger.exception('JSONDecodeError from Hydra',
                             extra={'doc': e.doc})
            raise
def send_key_to_datahub_service(task, subject, gsrn, session):
    """
    :param celery.Task task:
    :param str subject:
    :param str gsrn:
    :param sqlalchemy.orm.Session session:
    """
    __log_extra = {
        'subject': subject,
        'gsrn': gsrn,
        'pipeline': 'import_meteringpoints',
        'task': 'send_key_to_datahub_service',
    }

    # Get User from DB
    try:
        user = UserQuery(session) \
            .is_active() \
            .has_sub(subject) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to load User from database, retrying...',
                         extra=__log_extra)
        raise task.retry(exc=e)

    # Get MeteringPoint from DB
    try:
        meteringpoint = MeteringPointQuery(session) \
            .has_gsrn(gsrn) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception(
            'Failed to load MeteringPoint from database, retrying...',
            extra=__log_extra)
        raise task.retry(exc=e)

    # Send key to DataHubService
    try:
        datahub_service.set_key(
            token=user.access_token,
            gsrn=meteringpoint.gsrn,
            key=meteringpoint.extended_key,
        )
    except DataHubServiceConnectionError as e:
        logger.exception(
            f'Failed to establish connection to DataHubService, retrying...',
            extra=__log_extra)
        raise task.retry(exc=e)
    except DataHubServiceError as e:
        if e.status_code == 400:
            raise
        else:
            logger.exception('Failed to import MeteringPoints, retrying...',
                             extra=__log_extra)
            raise task.retry(exc=e)
예제 #3
0
def poll_batch_status(task, subject, batch_id, session):
    """
    :param celery.Task task:
    :param str subject:
    :param int batch_id:
    :param sqlalchemy.orm.Session session:
    """
    __log_extra = {
        'subject': subject,
        'batch_id': str(batch_id),
        'pipeline': 'submit_batch_to_ledger',
        'task': 'poll_batch_status',
    }

    # Get batch from DB
    try:
        batch = session \
            .query(Batch) \
            .filter(Batch.id == batch_id) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to load Batch from database', extra=__log_extra)
        raise task.retry(exc=e)

    # Get batch status from ledger
    try:
        response = ledger.get_batch_status(batch.handle)
    except ols.LedgerConnectionError as e:
        logger.exception('Failed to poll ledger for batch status, retrying...', extra=__log_extra)
        raise task.retry(exc=e)

    # Assert status
    if response.status == ols.BatchStatus.COMMITTED:
        logger.info('Ledger batch status: COMMITTED', extra=__log_extra)
    elif response.status == ols.BatchStatus.INVALID:
        # Raising exception triggers the ON ERROR task (rollback_batch())
        raise InvalidBatch('Invalid batch')
    elif response.status == ols.BatchStatus.UNKNOWN:
        logger.info('Ledger batch status: UNKNOWN', extra=__log_extra)
        raise task.retry()
    elif response.status == ols.BatchStatus.PENDING:
        logger.info('Ledger batch status: PENDING', extra=__log_extra)
        raise task.retry()
    else:
        raise RuntimeError('Unknown batch status returned, should NOT have happened!')
예제 #4
0
def submit_batch_to_ledger(task, subject, batch_id, session):
    """
    :param celery.Task task:
    :param str subject:
    :param int batch_id:
    :param sqlalchemy.orm.Session session:
    """
    __log_extra = {
        'subject': subject,
        'batch_id': str(batch_id),
        'pipeline': 'submit_batch_to_ledger',
        'task': 'submit_to_ledger',
    }

    # Get Batch from DB
    try:
        batch = session \
            .query(Batch) \
            .filter(Batch.id == batch_id) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        raise task.retry(exc=e)

    # Submit batch to ledger
    try:
        handle = ledger.execute_batch(batch.build_ledger_batch())
    except ols.LedgerConnectionError as e:
        logger.exception('Failed to submit batch to ledger, retrying...', extra=__log_extra)
        raise task.retry(exc=e)
    except ols.LedgerException as e:
        if e.code in (15, 17, 18):
            logger.exception(f'Ledger validator error (code {e.code}), retrying...', extra=__log_extra)
            raise task.retry(exc=e)
        elif e.code == 31:
            logger.info(f'Ledger queue is full, retrying...', extra=__log_extra)
            raise task.retry(exc=e)
        else:
            raise

    logger.info(f'Batch submitted to ledger', extra=__log_extra)

    return handle
예제 #5
0
def invoke_on_forecast_received(task, subject, forecast_id, subscription_id,
                                session, **logging_kwargs):
    """
    :param celery.Task task:
    :param str subject:
    :param int forecast_id:
    :param int subscription_id:
    :param sqlalchemy.orm.Session session:
    """
    __log_extra = logging_kwargs.copy()
    __log_extra.update({
        'subject': subject,
        'forecast_id': str(forecast_id),
        'subscription_id': str(subscription_id),
        'pipeline': 'webhooks',
        'task': 'invoke_on_forecast_received',
    })

    # Get Forecast from database
    try:
        forecast = ForecastQuery(session) \
            .has_id(forecast_id) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to load Forecast from database',
                         extra=__log_extra)
        raise task.retry(exc=e)

    # Get webhook subscription from database
    try:
        subscription = webhook_service.get_subscription(
            subscription_id, session)
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to load WebhookSubscription from database',
                         extra=__log_extra)
        raise task.retry(exc=e)

    # Publish event to webhook
    try:
        webhook_service.on_forecast_received(subscription, forecast)
    except WebhookConnectionError as e:
        logger.exception(
            'Failed to invoke webhook: ON_FORECAST_RECEIVED (Connection error)',
            extra=__log_extra)
        raise task.retry(exc=e)
    except WebhookError as e:
        logger.exception('Failed to invoke webhook: ON_FORECAST_RECEIVED',
                         extra=__log_extra)
        raise task.retry(exc=e)
예제 #6
0
    def refresh_token(self, refresh_token):
        """
        Fetches a new access token using a refresh token.

        Use self.get_id_token() to decode the ID token.

        :param str refresh_token:
        :rtype: collections.abc.Mapping
        """
        try:
            return self.client.refresh_token(
                url=HYDRA_TOKEN_ENDPOINT,
                refresh_token=refresh_token,
                verify=not DEBUG,
            )
        except json.decoder.JSONDecodeError as e:
            logger.exception('JSONDecodeError from Hydra',
                             extra={'doc': e.doc})
            raise
예제 #7
0
    def register_login_state(self):
        """
        Register a login state. Is used before redirecting the client
        to Hydra, to perform login. Returns a tuple of (login_url, state)
        where the state is used to identify the client when its redirected
        back to the callback URL.

        :rtype: (str, str)
        :returns: Tuple of (login_url, state)
        """
        try:
            return self.client.create_authorization_url(
                url=HYDRA_AUTH_ENDPOINT,
                redirect_uri=LOGIN_CALLBACK_URL,
            )
        except json.decoder.JSONDecodeError as e:
            logger.exception('JSONDecodeError from Hydra',
                             extra={'doc': e.doc})
            raise
예제 #8
0
def batch_on_rollback(task, subject, batch_id, session):
    """
    :param celery.Task task:
    :param str subject:
    :param int batch_id:
    :param sqlalchemy.orm.Session session:
    """
    try:
        session \
            .query(Batch) \
            .filter(Batch.id == batch_id) \
            .one() \
            .on_rollback()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to invoke on_rollback() on Batch', extra={
            'subject': subject,
            'batch_id': str(batch_id),
            'pipeline': 'submit_batch_to_ledger',
            'task': 'batch_on_rollback',
        })
        raise task.retry(exc=e)
def import_meteringpoints_and_insert_to_db(task, subject, session):
    """
    :param celery.Task task:
    :param str subject:
    :param sqlalchemy.orm.Session session:
    """
    __log_extra = {
        'subject': subject,
        'pipeline': 'import_meteringpoints',
        'task': 'import_meteringpoints_and_insert_to_db',
    }

    # Get User from DB
    try:
        user = UserQuery(session) \
            .is_active() \
            .has_sub(subject) \
            .one()
    except orm.exc.NoResultFound:
        raise
    except Exception as e:
        logger.exception('Failed to load User from database, retrying...',
                         extra=__log_extra)
        raise task.retry(exc=e)

    # Import MeteringPoints from DataHubService
    try:
        response = datahub_service.get_meteringpoints(user.access_token)
    except DataHubServiceConnectionError as e:
        logger.exception(
            f'Failed to establish connection to DataHubService, retrying...',
            extra=__log_extra)
        raise task.retry(exc=e)
    except DataHubServiceError as e:
        if e.status_code == 400:
            raise
        else:
            logger.exception('Failed to import MeteringPoints, retrying...',
                             extra=__log_extra)
            raise task.retry(exc=e)

    # Save imported MeteringPoints to database
    try:
        meteringpoints = save_imported_meteringpoints(user, response)
    except Exception as e:
        logger.exception(
            'Failed to save imported Meteringpoints to database, retrying...',
            extra=__log_extra)
        raise task.retry(exc=e)

    logger.info(
        f'Imported {len(meteringpoints)} new MeteringPoints from DataHubService',
        extra=__log_extra)

    # Send MeteringPoint key to DataHubService for each imported MeteringPoint
    tasks = []

    for meteringpoint in meteringpoints:
        logger.info(f'Imported meteringpoint with GSRN: {meteringpoint.gsrn}',
                    extra={
                        'gsrn': meteringpoint.gsrn,
                        'subject': user.sub,
                        'pipeline': 'import_meteringpoints',
                        'task': 'import_meteringpoints_and_insert_to_db',
                    })

        tasks.append(
            send_key_to_datahub_service.s(
                subject=subject,
                gsrn=meteringpoint.gsrn,
            ))

    group(*tasks).apply_async()
예제 #10
0
    def handle_request(self, request, session):
        """
        :param VerifyLoginCallbackRequest request:
        :param sqlalchemy.orm.Session session:
        :rtype: flask.Response
        """
        return_url = redis.get(request.state)

        if return_url is None:
            raise BadRequest('Click back in your browser')
        else:
            return_url = return_url.decode()
            redis.delete(request.state)

        # Fetch token
        try:
            token = backend.fetch_token(request.code, request.state)
        except:
            logger.exception(f'Failed to fetch token',
                             extra={
                                 'scope': str(request.scope),
                                 'code': request.code,
                                 'state': request.state,
                             })
            return self.redirect_to_failure(return_url)

        # Extract data from token
        id_token = backend.get_id_token(token)

        # No id_token means the user declined to give consent
        if id_token is None:
            return self.redirect_to_failure(return_url,
                                            'No ID token from Hydra')

        expires = datetime \
            .fromtimestamp(token['expires_at']) \
            .replace(tzinfo=timezone.utc)

        # Lookup user from "subject"
        user = UserQuery(session) \
            .is_active() \
            .has_sub(id_token['sub']) \
            .one_or_none()

        if user is None:
            logger.error(
                f'User login: Creating new user and subscribing to webhooks',
                extra={
                    'subject': id_token['sub'],
                })
            self.create_new_user(token, id_token, expires, session)
            self.datahub.webhook_on_meteringpoint_available_subscribe(
                token['access_token'])
            self.datahub.webhook_on_ggo_issued_subscribe(token['access_token'])
        else:
            logger.error(f'User login: Updating tokens for existing user',
                         extra={
                             'subject': id_token['sub'],
                         })
            self.update_user_attributes(user, token, expires)

        # Create HTTP response
        response = redirect(f'{return_url}?success=1', code=303)
        response.headers[
            'Cache-Control'] = 'no-cache, no-store, must-revalidate'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '0'
        response.headers['Cache-Control'] = 'public, max-age=0'

        return response