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))
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))