Beispiel #1
0
    def method_preperation_hook(self, method, *args, **kwargs):
        """Try to read request body as JSON.

        The parsed data will be available as `self.json`.
        """
        rawdata = self.request.body
        data = None
        if not rawdata:
            data = None
        elif self.request.headers['Content-Type'].startswith('application/json'):
            data = hujson2.loads(rawdata)
        else:
            # some strange stuff is being sent
            if self.request.headers.get('Content-Type').startswith(
                'application/x-www-form-urlencoded'
            ):
                data = hujson2.loads(
                    urllib.unquote_plus(self.request.body).strip('=\n')
                )
        self.json = data
        # TODO: use self.request.json
        sentry_client.note('rpc', message='JSON API Call', data=dict(data=data))
Beispiel #2
0
    def dispatch(self):
        """Dispatches the requested method fom the WSGI App.

        Meant for internal use by the stack.
        """
        request = self.request
        method_name = request.route.handler_method
        if not method_name:
            method_name = webapp2._normalize_handler_method(request.method)

        method = getattr(self, method_name, None)
        if hasattr(self, '__class__'):
            sentry_client.tags_context({
                'handler': self.__class__.__name__,
                'method': method_name
            })

        if method is None:
            # 405 Method Not Allowed.
            valid = b', '.join(webapp2._get_handler_methods(self))
            raise exc.HTTP405_HTTPMethodNotAllowed(
                'Method not allowed in {}'.format(self.__class__.__name__),
                headers=[(b'Allow', valid)],
            )

        # The handler only receives *args if no named variables are set.
        args, kwargs = request.route_args, request.route_kwargs
        if kwargs:
            args = ()

        # bind session on dispatch (not in __init__)
        try:
            self.session = gaesessions.get_current_session()
        except AttributeError:
            # probably session middleware not loaded
            self.session = {}

        if str(self.session) != 'uninitialized session':
            sentry_client.note('storage',
                               'Session loaded',
                               data=dict(session=self.session))

        try:
            self._call_all_inherited('pre_authentication_hook', method_name,
                                     *args, **kwargs)
            self._call_all_inherited('authentication_preflight_hook',
                                     method_name, *args, **kwargs)
            self._call_all_inherited('authentication_hook', method_name, *args,
                                     **kwargs)
            self._call_all_inherited('authorisation_hook', method_name, *args,
                                     **kwargs)
            self._call_all_inherited('method_preperation_hook', method_name,
                                     *args, **kwargs)
            try:
                response = method(*args, **kwargs)
            except TypeError:
                # parameter missmatch is the error we see most often
                # so help to pin down where it happens
                klass = introspection.get_class_that_defined_method(method)
                methname = method.__name__
                sourcepos = '{}:{}'.format(
                    os.path.basename(method.__func__.__code__.co_filename),
                    method.__func__.__code__.co_firstlineno,
                )
                LOGGER.debug(
                    'method called: %s.%s(%r) from %s',
                    klass.__name__,
                    methname,
                    (args, kwargs),
                    sourcepos,
                )
                LOGGER.debug('defined at: %s %s', klass, sourcepos)
                raise
            response = self.response_overwrite(response, method, *args,
                                               **kwargs)
        except exc.HTTPException as e:
            # for HTTP exceptions execute `finished_hooks`
            if e.code < 500:
                self._call_all_inherited('finished_hook', method_name, *args,
                                         **kwargs)
            return self.handle_exception(e, self.app.debug)
        except BaseException as e:
            return self.handle_exception(e, self.app.debug)

        if response and not getattr(self, '_gaetk2_allow_strange_responses',
                                    False):
            assert isinstance(response, webapp2.Response)

        self._set_cache_headers()
        self._call_all_inherited('finished_hook', method_name, *args, **kwargs)
        self.finished_overwrite(response, method, *args, **kwargs)
        return response
    def handle_exception(self, request, response, e):
        """Handles a uncaught exception occurred in :meth:`__call__`.

        Uncaught exceptions can be handled by error handlers registered in
        :attr:`error_handlers`. This is a dictionary that maps HTTP status
        codes to callables that will handle the corresponding error code.
        If the exception is not an ``HTTPException``, the status code 500
        is used.

        The error handlers receive (request, response, exception) and can be
        a callable or a string in dotted notation to be lazily imported.

        If no error handler is found, the exception is re-raised.

        Parameters:
            request: A :class:`Request` instance.
            response: A :class:`Response` instance.
            e: The uncaught exception.

        Returns:
            The returned value from the error handler.

        """
        if isinstance(e, exc.HTTPException):
            code = e.code
        else:
            code = 500

        # WSGIHTTPException come with some further explanations
        notedata = {}
        for attr in ['code', 'explanation', 'detail', 'comment', 'headers']:
            if getattr(e, attr, None):
                notedata[attr] = getattr(e, attr)
        if notedata:
            sentry_client.note('navigation',
                               message='HTTPException',
                               data=notedata)

        handler = self.error_handlers.get(code)
        if handler and not os.environ.get('GAETK2_WEBTEST'):
            LOGGER.debug('using %s', handler)
            return handler(request, response, e)
        else:
            if getattr(e, '_want_trackback', False) or code >= 500:
                # Our default handler
                return self.default_exception_handler(request, response, e)
            else:
                # This should be mostly `exc.HTTPException`s < 500
                # which will be rendered directly to the client
                # but if we have a `explanation` we really want to
                # note that in Sentry:
                if getattr(e, 'explanation', None):
                    notedata.update(self.get_sentry_addon(request))
                    # TODO: http-Headers etc are missing
                    sentry_client.captureMessage(
                        'HTTPException {}: {}'.format(code, e.explanation),
                        level='info',
                        tags={
                            'httpcode': code,
                            'type': 'Exception'
                        },
                        extra=notedata,
                    )
                logging.debug('HTTP exception:', exc_info=True)
                raise  # pylint: disable=E0704
    def authentication_preflight_hook(self, method, *args, **kwargs):
        """Find out if the User is authenticated in any sane way and if so load the credential."""
        # Automatically called by `dispatch`.
        # This funcion is somewhat involved. We accept
        # 1) Authentication via HTTP-Auth
        # 2) Authentication via JWT
        # 3) Check for App Engine / Google Apps based Login
        # 4) Authentication via Session
        # 6) Other App Engine Apps
        # 7) log in by Sentry bot
        # 8) Our Test Framework
        self.credential = None
        uid, secret = None, None
        # 1. Check for valid HTTP-Basic Auth Login
        if self.request.headers.get('Authorization',
                                    '').lower().startswith('basic '):
            auth_type, encoded = self.request.headers.get(
                'Authorization').split(None, 1)
            decoded = encoded.decode('base64')
            # If the Client send us invalid credentials, let him know , else parse into
            # username and password
            if ':' not in decoded:
                raise exc.HTTP400_BadRequest(
                    explanation='invalid credentials %r' % decoded)
            uid, secret = decoded.strip().split(':', 1)
            sentry_client.note('user',
                               'HTTP-Auth attempted for %r' % uid,
                               data=dict(auth_typ=auth_type, decoded=decoded))

            self.credential = self.get_credential(uid or ' *invalid* ')
            if self.credential and self.credential.secret == secret.strip():
                # Successful login
                return self._login_user('HTTP')
            else:
                logger.error('failed HTTP-Login from %s/%s %s %s %r', uid,
                             self.request.remote_addr,
                             self.request.headers.get('Authorization'),
                             self.credential, secret.strip())
                logger.info('Falsches Credential oder kein secret')
                raise exc.HTTP401_Unauthorized(
                    'Invalid HTTP-Auth Infomation',
                    headers={b'WWW-Authenticate': b'Basic realm="API Login"'})

        # 2. Check for valid Authentication via JWT via GAETK2 or Auth0 Tokens
        if self.request.headers.get('Authorization',
                                    '').lower().startswith('bearer '):
            auth_type, access_token = self.request.headers.get(
                'Authorization').split(None, 1)
            # non standards extension to allow us to transport access and id token
            id_token = None
            if ' ' in access_token:
                access_token, id_token = access_token.split(None, 1)
            logging.debug('token: %s', access_token)
            unverified_header = self.jose_jwt_get_unverified_header(
                access_token)
            if gaetkconfig.JWT_SECRET_KEY and unverified_header[
                    'alg'] == 'HS256':
                # gaetk2 JWT for internal use
                userdata = self.decode_jwt(access_token,
                                           gaetkconfig.JWT_SECRET_KEY,
                                           algorithms=['HS256'])
                # 'sub': u'*****@*****.**'}
                self.credential = check404(
                    self.get_credential(userdata['sub']),
                    'unknown uid %r' % userdata['sub'])
                # We do not check the password because get_current_user() is trusted
                return self._login_user('gaetk2 JWT')

            else:
                # Auth0 JWT, see http://filez.foxel.org/2P2o1O3j2f1N
                userdata = self.decode_jwt(access_token,
                                           get_jwt_key(
                                               unverified_header['kid']),
                                           algorithms=['RS256'])
                # For Auth0 / OpenID authentication there should be an user in our database
                self.credential = models.gaetk_Credential.query(
                    models.gaetk_Credential.external_uid ==
                    userdata['sub']).get()
                email = None
                if not self.credential:
                    # if not: can we find one by id_token?
                    if userdata.get('email_verified'):
                        email = userdata.get('email')
                    else:
                        if id_token:
                            userdata = self.decode_jwt(
                                id_token,
                                get_jwt_key(unverified_header['kid']),
                                algorithms=['RS256'],
                                access_token=access_token)
                            if userdata.get('email_verified'):
                                email = userdata.get('email')
                    if email:
                        self.credential = models.gaetk_Credential.query(
                            models.gaetk_Credential.email == email).get()
                        logging.info('%s zugeordnet', self.credential)
                        # if self.credential:
                        #     self.credential.external_uid = userdata['sub']
                        #     self.credential.put()
                self.credential = check404(self.credential,
                                           '%s not found' % userdata['sub'])
                # We do not check the password because get_current_user() is trusted
                return self._login_user('Auth0 JWT')

        # 3. Check for App Engine / Google Apps based Login
        user = users.get_current_user()
        if user:
            self.credential = self.get_credential(user.email())
            if self.credential:
                # We do not check the password because get_current_user() is trusted
                return self._login_user('Google User OpenID Connect')

        # 4. Check for session based login
        if self.session.get('uid'):
            logger.debug('trying session based login')
            self.credential = self.get_credential(self.session['uid'])
            if self.credential:
                # We do not check the password because session storage is trusted
                return self._login_user('session')
            else:
                logger.warn(
                    'No Credential for Session: %s. Progressing unauthenticated',
                    self.session.get('uid'))
                self._clear_session()

        # 5. Login for Google Special Calls from Cron & TaskQueue
        # X-AppEngine-QueueName
        # https://cloud.google.com/appengine/docs/standard/python/config/cron#securing_urls_for_cron
        # TODO:
        # x-appengine-user-is-admin
        # x-appengine-auth-domain
        # x-google-real-ip
        # https://cloud.google.com/appengine/docs/standard/python/appidentity/
        # X-Appengine-Cron: true
        if self.request.headers.get('X-AppEngine-QueueName'):
            uid = 'X-AppEngine-Taskqueue-{}'.format(
                self.request.headers.get('X-AppEngine-QueueName'))
            self.credential = models.gaetk_Credential.create(
                id=uid, uid=uid, text='created automatically via gaetk2')
            return self._login_user('AppEngine')

        # 6. Other App Engine Apps
        # X-Appengine-Inbound-Appid
        # https://cloud.google.com/appengine/docs/standard/python/appidentity/#asserting_identity_to_other_app_engine_apps
        ibaid = self.request.headers.get('X-Appengine-Inbound-Appid')
        if ibaid:
            if ibaid in gaetkconfig.INBOUND_APP_IDS:
                uid = 'X-Appengine-Inbound-Appid-{}'.format(ibaid)
                self.credential = models.gaetk_Credential.create(
                    id=uid, uid=uid, text='created automatically via gaetk2')
                return self._login_user('AppEngine')
            else:
                logging.debug(
                    'configure INBOUND_APP_IDS to allow %s access',
                    self.request.headers.get('X-Appengine-Inbound-Appid'))

        # 7. log in by Sentry bot
        # see https://blog.sentry.io/2017/06/15/notice-of-address-change
        if self.request.headers.get('X-Sentry-Token'):
            if gaetkconfig.SENTRY_SECURITY_TOKEN:
                if self.request.headers.get(
                        'X-Sentry-Token') == gaetkconfig.SENTRY_SECURITY_TOKEN:
                    uid = '*****@*****.**'
                    self.credential = models.gaetk_Credential.create(
                        id=uid,
                        uid=uid,
                        text='created automatically via gaetk2')
                    return self._login_user('Sentry')

        # 8. Test Framework
        # see https://blog.sentry.io/2017/06/15/notice-of-address-change
        if os.environ.get('GAETK2_WEBTEST'):
            uid = '*****@*****.**'
            self.credential = models.gaetk_Credential.create(
                id=uid, uid=uid, text='created automatically via gaetk2')
            return self._login_user('GAETK2_WEBTEST')

        logger.info(
            'user unauthenticated: Authorization:%r User:%r Session:%r QueueName:%r Appid:%r Sentry:%r',
            self.request.headers.get('Authorization', ''),
            users.get_current_user(),
            self.session,
            self.request.headers.get('X-AppEngine-QueueName'),
            self.request.headers.get('X-Appengine-Inbound-Appid'),
            self.request.headers.get('X-Sentry-Token'),
        )
        logger.debug('headers: %s', self.request.headers.items())
    def _login_user(self, via, jwtinfo=None):
        """Ensure the system knows that a user has been logged in."""
        # user did not exist before but we have a validated jwt
        sentry_client.note('user',
                           'logging in via %s' % via,
                           data=dict(jwtinfo=jwtinfo,
                                     credential=self.credential))
        if not self.credential and jwtinfo:
            # create credential from JWT
            self.credential = allow_credential_from_jwt(jwtinfo)
            if not self.credential:
                # here we could redirect the user to a page
                # explaining that we couldn't match the data
                # given by him to our local database.
                raise exc.HTTP401_Unauthorized(
                    explanation="Couldn't assign {} to a local user".format(
                        jwtinfo))

        # ensure that users with empty password are never logged in
        if self.credential and not self.credential.secret:
            raise exc.HTTP403_Forbidden(explanation='Account %s disabled' %
                                        self.credential.uid)

        if not self.credential:
            raise RuntimeError('unknown user via %r' % via)

        if 'uid' not in self.session or self.session[
                'uid'] != self.credential.uid:
            self.session['uid'] = self.credential.uid
        if 'login_via' not in self.session:
            self.session['login_via'] = via
        if 'login_time' not in self.session:
            self.session['login_time'] = datetime.datetime.now()
        # if AppEngine did not set Variables, we fake them
        # this is helpful for things like `ndb.UserProperty(auto_current_user=True)`
        if not os.environ.get('USER_ID', None):
            os.environ['USER_ID'] = self.credential.uid
            if not os.environ.get('AUTH_DOMAIN'):
                os.environ['AUTH_DOMAIN'] = 'auth.gaetk2.23.nu'
            # os.environ['USER_IS_ADMIN'] = credential.admin
            if self.credential.email:
                os.environ['USER_EMAIL'] = self.credential.email
            else:
                # fake email-address
                os.environ[
                    'USER_EMAIL'] = '*****@*****.**' % self.credential.uid

        sentry_client.note('user',
                           '{} logged in via {} since {} sid:{}'.format(
                               self.credential.uid,
                               self.session.get('login_via'),
                               self.session.get('login_time'),
                               getattr(self.session, 'sid', '?')),
                           data=dict(credential=self.credential,
                                     login_via=self.session.get('login_via'),
                                     login_time=self.session.get('login_time'),
                                     sid=getattr(self.session, 'sid', '?')))
        sentry_client.user_context({
            'email': os.environ.get('USER_EMAIL'),
            'id': self.credential.uid,
            'username': self.credential.name,
            # HTTP_X_APPENGINE_CRON   true
            # USER_ORGANIZATION USER_IS_ADMIN
        })
        self.response.headers.add_header(b'X-uid', str(self.credential.uid))