def create_router(self): backends = [] for provider in BACKEND_NAMES: backends.append( TestBackend(None, {"attributes": {}}, None, None, provider)) frontends = [] for receiver in FRONTEND_NAMES: frontends.append( TestFrontend(None, {"attributes": {}}, None, None, receiver)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" microservices = [ TestRequestMicroservice(request_micro_service_name, base_url="https://satosa.example.com"), TestResponseMicroservice(response_micro_service_name, base_url="https://satosa.example.com") ] self.router = ModuleRouter(frontends, backends, microservices)
def create_router(self): backends = [] for provider in BACKEND_NAMES: backends.append(TestBackend(None, {"attributes": {}}, None, None, provider)) frontends = [] for receiver in FRONTEND_NAMES: frontends.append(TestFrontend(None, {"attributes": {}}, None, None, receiver)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" microservices = [TestRequestMicroservice(request_micro_service_name, base_url="https://satosa.example.com"), TestResponseMicroservice(response_micro_service_name, base_url="https://satosa.example.com")] self.router = ModuleRouter(frontends, backends, microservices)
def __init__(self, config): """ Creates a satosa proxy base :type config: satosa.satosa_config.SATOSAConfig :param config: satosa proxy config """ if config is None: raise ValueError("Missing configuration") self.config = config LOGGER.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, self.config.INTERNAL_ATTRIBUTES) LOGGER.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, self.config.INTERNAL_ATTRIBUTES) self.consent_module = ConsentModule(config, self._consent_resp_callback_func) self.account_linking_module = AccountLinkingModule( config, self._account_linking_callback_func) # TODO register consent_module endpoints to module_router. Just add to backend list? if self.consent_module.enabled: backends["consent"] = self.consent_module if self.account_linking_module.enabled: backends["account_linking"] = self.account_linking_module LOGGER.info("Loading micro services...") self.request_micro_services = None self.response_micro_services = None if "MICRO_SERVICES" in self.config: self.request_micro_services, self.response_micro_services = load_micro_services( self.config.PLUGIN_PATH, self.config.MICRO_SERVICES, self.config.INTERNAL_ATTRIBUTES) self.module_router = ModuleRouter(frontends, backends)
def router_fixture(): frontends = {} backends = {} for provider in PROVIDERS: backends[provider] = FakeBackend( internal_attributes=INTERNAL_ATTRIBUTES) backends[ provider].register_endpoints_func = create_backend_endpoint_func( provider) for receiver in RECEIVERS: frontends[receiver] = FakeFrontend( internal_attributes=INTERNAL_ATTRIBUTES) frontends[ receiver].register_endpoints_func = create_frontend_endpoint_func( receiver) return ModuleRouter(frontends, backends), frontends, backends
def __init__(self, config): """ Creates a satosa proxy base :type config: satosa.satosa_config.SATOSAConfig :param config: satosa proxy config """ if config is None: raise ValueError("Missing configuration") self.config = config LOGGER.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, self.config.INTERNAL_ATTRIBUTES) LOGGER.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, self.config.INTERNAL_ATTRIBUTES) self.consent_module = ConsentModule(config, self._consent_resp_callback_func) self.account_linking_module = AccountLinkingModule(config, self._account_linking_callback_func) # TODO register consent_module endpoints to module_router. Just add to backend list? if self.consent_module.enabled: backends["consent"] = self.consent_module if self.account_linking_module.enabled: backends["account_linking"] = self.account_linking_module LOGGER.info("Loading micro services...") self.request_micro_services = None self.response_micro_services = None if "MICRO_SERVICES" in self.config: self.request_micro_services, self.response_micro_services = load_micro_services( self.config.PLUGIN_PATH, self.config.MICRO_SERVICES, self.config.INTERNAL_ATTRIBUTES) self.module_router = ModuleRouter(frontends, backends)
class TestModuleRouter: @pytest.fixture(autouse=True) def create_router(self): backends = [] for provider in BACKEND_NAMES: backends.append(TestBackend(None, {"attributes": {}}, None, None, provider)) frontends = [] for receiver in FRONTEND_NAMES: frontends.append(TestFrontend(None, {"attributes": {}}, None, None, receiver)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" microservices = [TestRequestMicroservice(request_micro_service_name, base_url="https://satosa.example.com"), TestResponseMicroservice(response_micro_service_name, base_url="https://satosa.example.com")] self.router = ModuleRouter(frontends, backends, microservices) @pytest.mark.parametrize('url_path, expected_frontend, expected_backend', [ ("%s/%s/request" % (provider, receiver), receiver, provider) for receiver in FRONTEND_NAMES for provider in BACKEND_NAMES ]) def test_endpoint_routing_to_frontend(self, url_path, expected_frontend, expected_backend): context = Context() context.path = url_path self.router.endpoint_routing(context) assert context.target_frontend == expected_frontend assert context.target_backend == expected_backend @pytest.mark.parametrize('url_path, expected_backend', [ ("%s/response" % (provider,), provider) for provider in BACKEND_NAMES ]) def test_endpoint_routing_to_backend(self, url_path, expected_backend): context = Context() context.path = url_path self.router.endpoint_routing(context) assert context.target_backend == expected_backend assert context.target_frontend is None @pytest.mark.parametrize('url_path, expected_micro_service', [ ("request_microservice/callback", "RequestService"), ("response_microservice/callback", "ResponseService") ]) def test_endpoint_routing_to_microservice(self, url_path, expected_micro_service): context = Context() context.path = url_path microservice_callable = self.router.endpoint_routing(context) assert context.target_micro_service == expected_micro_service assert microservice_callable == self.router.micro_services[expected_micro_service]["instance"].callback assert context.target_backend is None assert context.target_frontend is None @pytest.mark.parametrize('url_path, expected_frontend, expected_backend', [ ("%s/%s/request" % (provider, receiver), receiver, provider) for receiver in FRONTEND_NAMES for provider in BACKEND_NAMES ]) def test_module_routing(self, url_path, expected_frontend, expected_backend, context): context.path = url_path self.router.endpoint_routing(context) assert context.target_backend == expected_backend assert context.target_frontend == expected_frontend backend = self.router.backend_routing(context) assert backend == self.router.backends[expected_backend]["instance"] frontend = self.router.frontend_routing(context) assert frontend == self.router.frontends[expected_frontend]["instance"] def test_endpoint_routing_with_unknown_endpoint(self, context): context.path = "unknown" with pytest.raises(SATOSANoBoundEndpointError): self.router.endpoint_routing(context) @pytest.mark.parametrize(("frontends", "backends", "micro_services"), [ (None, None, {}), ({}, {}, {}), ]) def test_bad_init(self, frontends, backends, micro_services): with pytest.raises(ValueError): ModuleRouter(frontends, backends, micro_services)
class SATOSABase(object): """ Base class for a satosa proxy server. Does not contain any server parts. """ STATE_KEY = "SATOSA_REQUESTOR" def __init__(self, config): """ Creates a satosa proxy base :type config: satosa.satosa_config.SATOSAConfig :param config: satosa proxy config """ if config is None: raise ValueError("Missing configuration") self.config = config LOGGER.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, self.config.INTERNAL_ATTRIBUTES) LOGGER.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, self.config.INTERNAL_ATTRIBUTES) self.consent_module = ConsentModule(config, self._consent_resp_callback_func) self.account_linking_module = AccountLinkingModule(config, self._account_linking_callback_func) # TODO register consent_module endpoints to module_router. Just add to backend list? if self.consent_module.enabled: backends["consent"] = self.consent_module if self.account_linking_module.enabled: backends["account_linking"] = self.account_linking_module LOGGER.info("Loading micro services...") self.request_micro_services = None self.response_micro_services = None if "MICRO_SERVICES" in self.config: self.request_micro_services, self.response_micro_services = load_micro_services( self.config.PLUGIN_PATH, self.config.MICRO_SERVICES, self.config.INTERNAL_ATTRIBUTES) self.module_router = ModuleRouter(frontends, backends) def _auth_req_callback_func(self, context, internal_request): """ This function is called by a frontend module when an authorization request has been processed. :type context: satosa.context.Context :type internal_request: satosa.internal_data.InternalRequest :rtype: satosa.response.Response :param context: The request context :param internal_request: request processed by the frontend :return: response """ state = context.state state.add(SATOSABase.STATE_KEY, internal_request.requestor) satosa_logging(LOGGER, logging.INFO, "Requesting provider: {}".format(internal_request.requestor), state) context.request = None backend = self.module_router.backend_routing(context) self.consent_module.save_state(internal_request, state) UserIdHasher.save_state(internal_request, state) if self.request_micro_services: internal_request = self.request_micro_services.process_service_queue(context, internal_request) return backend.start_auth(context, internal_request) def _auth_resp_callback_func(self, context, internal_response): """ This function is called by a backend module when the authorization is complete. :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The request context :param internal_response: The authentication response :return: response """ context.request = None internal_response.to_requestor = context.state.get(SATOSABase.STATE_KEY) user_id_attr = self.config.INTERNAL_ATTRIBUTES.get("user_id_from_attr", []) if user_id_attr: internal_response.set_user_id_from_attr(user_id_attr) # Hash the user id user_id = UserIdHasher.hash_data(self.config.USER_ID_HASH_SALT, internal_response.get_user_id()) internal_response.set_user_id(user_id) if self.response_micro_services: internal_response = \ self.response_micro_services.process_service_queue(context, internal_response) return self.account_linking_module.manage_al(context, internal_response) def _account_linking_callback_func(self, context, internal_response): """ This function is called by the account linking module when the linking step is done :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The response context :param internal_response: The authentication response :return: response """ user_id = UserIdHasher.hash_id(self.config.USER_ID_HASH_SALT, internal_response.get_user_id(), internal_response.to_requestor, context.state) internal_response.set_user_id(user_id) internal_response.set_user_id_hash_type(UserIdHasher.hash_type(context.state)) user_id_to_attr = self.config.INTERNAL_ATTRIBUTES.get("user_id_to_attr", None) if user_id_to_attr: attributes = internal_response.get_attributes() attributes[user_id_to_attr] = internal_response.get_user_id() internal_response.add_attributes(attributes) # Hash all attributes specified in INTERNAL_ATTRIBUTES["hash] hash_attributes = self.config.INTERNAL_ATTRIBUTES.get("hash", []) internal_attributes = internal_response.get_attributes() for attribute in hash_attributes: internal_attributes[attribute] = UserIdHasher.hash_data(self.config.USER_ID_HASH_SALT, internal_attributes[attribute]) return self.consent_module.manage_consent(context, internal_response) def _consent_resp_callback_func(self, context, internal_response): """ This function is called by the consent module when the consent step is done :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The response context :param internal_response: The authentication response :return: response """ context.request = None context.state.set_delete_state() frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) def _handle_satosa_authentication_error(self, error): """ Sends a response to the requestor about the error :type error: satosa.exception.SATOSAAuthenticationError :rtype: satosa.response.Response :param error: The exception :return: response """ context = Context() context.state = error.state frontend = self.module_router.frontend_routing(context) return frontend.handle_backend_error(error) def _run_bound_endpoint(self, context, spec): """ :type context: satosa.context.Context :type spec: ((satosa.context.Context, Any) -> satosa.response.Response, Any) | (satosa.context.Context) -> satosa.response.Response :param context: The request context :param spec: bound endpoint function :return: response """ try: if isinstance(spec, tuple): return spec[0](context, *spec[1:]) else: return spec(context) except SATOSAAuthenticationError as error: error.error_id = uuid4().urn msg = "ERROR_ID [{err_id}]\nSTATE:\n{state}".format(err_id=error.error_id, state=json.dumps( error.state.state_dict, indent=4)) satosa_logging(LOGGER, logging.ERROR, msg, error.state, exc_info=True) return self._handle_satosa_authentication_error(error) def _load_state(self, context): """ Load a state to the context :type context: satosa.context.Context :param context: Session context """ try: state = cookie_to_state(context.cookie, self.config.COOKIE_STATE_NAME, self.config.STATE_ENCRYPTION_KEY) except SATOSAStateError: state = State() context.state = state def _save_state(self, resp, context): """ Saves a state from context to cookie :type resp: satosa.response.Response :type context: satosa.context.Context :param resp: The response :param context: Session context """ if context.state.should_delete(): # Save empty state with a max age of 0 cookie = state_to_cookie(State(), self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY, 0) else: cookie = state_to_cookie(context.state, self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY) if isinstance(resp, Response): resp.add_cookie(cookie) else: try: resp.headers.append(tuple(cookie.output().split(": ", 1))) except: satosa_logging(LOGGER, logging.WARN, "can't add cookie to response '%s'" % resp.__class__, context.state) pass def run(self, context): """ Runs the satosa proxy with the given context. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The request context :return: response """ try: self._load_state(context) spec = self.module_router.endpoint_routing(context) resp = self._run_bound_endpoint(context, spec) self._save_state(resp, context) except SATOSAError: satosa_logging(LOGGER, logging.ERROR, "Uncaught SATOSA error", context.state, exc_info=True) raise except Exception as err: satosa_logging(LOGGER, logging.ERROR, "Uncaught exception", context.state, exc_info=True) raise SATOSAUnknownError("Unknown error") from err return resp
def test_bad_init(self, frontends, backends, micro_services): with pytest.raises(ValueError): ModuleRouter(frontends, backends, micro_services)
class TestModuleRouter: @pytest.fixture(autouse=True) def create_router(self): backends = [] for provider in BACKEND_NAMES: backends.append( TestBackend(None, {"attributes": {}}, None, None, provider)) frontends = [] for receiver in FRONTEND_NAMES: frontends.append( TestFrontend(None, {"attributes": {}}, None, None, receiver)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" microservices = [ TestRequestMicroservice(request_micro_service_name, base_url="https://satosa.example.com"), TestResponseMicroservice(response_micro_service_name, base_url="https://satosa.example.com") ] self.router = ModuleRouter(frontends, backends, microservices) @pytest.mark.parametrize('url_path, expected_frontend, expected_backend', [("%s/%s/request" % (provider, receiver), receiver, provider) for receiver in FRONTEND_NAMES for provider in BACKEND_NAMES]) def test_endpoint_routing_to_frontend(self, url_path, expected_frontend, expected_backend): context = Context() context.path = url_path self.router.endpoint_routing(context) assert context.target_frontend == expected_frontend assert context.target_backend == expected_backend @pytest.mark.parametrize('url_path, expected_backend', [("%s/response" % (provider, ), provider) for provider in BACKEND_NAMES]) def test_endpoint_routing_to_backend(self, url_path, expected_backend): context = Context() context.path = url_path self.router.endpoint_routing(context) assert context.target_backend == expected_backend assert context.target_frontend is None @pytest.mark.parametrize( 'url_path, expected_micro_service', [("request_microservice/callback", "RequestService"), ("response_microservice/callback", "ResponseService")]) def test_endpoint_routing_to_microservice(self, url_path, expected_micro_service): context = Context() context.path = url_path microservice_callable = self.router.endpoint_routing(context) assert context.target_micro_service == expected_micro_service assert microservice_callable == self.router.micro_services[ expected_micro_service]["instance"].callback assert context.target_backend is None assert context.target_frontend is None @pytest.mark.parametrize('url_path, expected_frontend, expected_backend', [("%s/%s/request" % (provider, receiver), receiver, provider) for receiver in FRONTEND_NAMES for provider in BACKEND_NAMES]) def test_module_routing(self, url_path, expected_frontend, expected_backend, context): context.path = url_path self.router.endpoint_routing(context) assert context.target_backend == expected_backend assert context.target_frontend == expected_frontend backend = self.router.backend_routing(context) assert backend == self.router.backends[expected_backend]["instance"] frontend = self.router.frontend_routing(context) assert frontend == self.router.frontends[expected_frontend]["instance"] def test_endpoint_routing_with_unknown_endpoint(self, context): context.path = "unknown" with pytest.raises(SATOSANoBoundEndpointError): self.router.endpoint_routing(context) @pytest.mark.parametrize(("frontends", "backends", "micro_services"), [ (None, None, {}), ({}, {}, {}), ]) def test_bad_init(self, frontends, backends, micro_services): with pytest.raises(ValueError): ModuleRouter(frontends, backends, micro_services)
def test_bad_init(frontends, backends): with pytest.raises(ValueError): ModuleRouter(frontends, backends)
class SATOSABase(object): """ Base class for a satosa proxy server. Does not contain any server parts. """ STATE_KEY = "SATOSA_REQUESTOR" def __init__(self, config): """ Creates a satosa proxy base :type config: satosa.satosa_config.SATOSAConfig :param config: satosa proxy config """ if config is None: raise ValueError("Missing configuration") self.config = config LOGGER.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, self.config.INTERNAL_ATTRIBUTES) LOGGER.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, self.config.INTERNAL_ATTRIBUTES) self.consent_module = ConsentModule(config, self._consent_resp_callback_func) self.account_linking_module = AccountLinkingModule( config, self._account_linking_callback_func) # TODO register consent_module endpoints to module_router. Just add to backend list? if self.consent_module.enabled: backends["consent"] = self.consent_module if self.account_linking_module.enabled: backends["account_linking"] = self.account_linking_module LOGGER.info("Loading micro services...") self.request_micro_services = None self.response_micro_services = None if "MICRO_SERVICES" in self.config: self.request_micro_services, self.response_micro_services = load_micro_services( self.config.PLUGIN_PATH, self.config.MICRO_SERVICES, self.config.INTERNAL_ATTRIBUTES) self.module_router = ModuleRouter(frontends, backends) def _auth_req_callback_func(self, context, internal_request): """ This function is called by a frontend module when an authorization request has been processed. :type context: satosa.context.Context :type internal_request: satosa.internal_data.InternalRequest :rtype: satosa.response.Response :param context: The request context :param internal_request: request processed by the frontend :return: response """ state = context.state state.add(SATOSABase.STATE_KEY, internal_request.requestor) satosa_logging( LOGGER, logging.INFO, "Requesting provider: {}".format(internal_request.requestor), state) context.request = None backend = self.module_router.backend_routing(context) self.consent_module.save_state(internal_request, state) UserIdHasher.save_state(internal_request, state) if self.request_micro_services: internal_request = self.request_micro_services.process_service_queue( context, internal_request) return backend.start_auth(context, internal_request) def _auth_resp_callback_func(self, context, internal_response): """ This function is called by a backend module when the authorization is complete. :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The request context :param internal_response: The authentication response :return: response """ context.request = None internal_response.to_requestor = context.state.get( SATOSABase.STATE_KEY) user_id_attr = self.config.INTERNAL_ATTRIBUTES.get( "user_id_from_attr", []) if user_id_attr: internal_response.set_user_id_from_attr(user_id_attr) # Hash the user id user_id = UserIdHasher.hash_data(self.config.USER_ID_HASH_SALT, internal_response.get_user_id()) internal_response.set_user_id(user_id) if self.response_micro_services: internal_response = \ self.response_micro_services.process_service_queue(context, internal_response) return self.account_linking_module.manage_al(context, internal_response) def _account_linking_callback_func(self, context, internal_response): """ This function is called by the account linking module when the linking step is done :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The response context :param internal_response: The authentication response :return: response """ user_id = UserIdHasher.hash_id(self.config.USER_ID_HASH_SALT, internal_response.get_user_id(), internal_response.to_requestor, context.state) internal_response.set_user_id(user_id) internal_response.set_user_id_hash_type( UserIdHasher.hash_type(context.state)) user_id_to_attr = self.config.INTERNAL_ATTRIBUTES.get( "user_id_to_attr", None) if user_id_to_attr: attributes = internal_response.get_attributes() attributes[user_id_to_attr] = internal_response.get_user_id() internal_response.add_attributes(attributes) # Hash all attributes specified in INTERNAL_ATTRIBUTES["hash] hash_attributes = self.config.INTERNAL_ATTRIBUTES.get("hash", []) internal_attributes = internal_response.get_attributes() for attribute in hash_attributes: internal_attributes[attribute] = UserIdHasher.hash_data( self.config.USER_ID_HASH_SALT, internal_attributes[attribute]) return self.consent_module.manage_consent(context, internal_response) def _consent_resp_callback_func(self, context, internal_response): """ This function is called by the consent module when the consent step is done :type context: satosa.context.Context :type internal_response: satosa.internal_data.InternalResponse :rtype: satosa.response.Response :param context: The response context :param internal_response: The authentication response :return: response """ context.request = None context.state.set_delete_state() frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) def _handle_satosa_authentication_error(self, error): """ Sends a response to the requestor about the error :type error: satosa.exception.SATOSAAuthenticationError :rtype: satosa.response.Response :param error: The exception :return: response """ context = Context() context.state = error.state frontend = self.module_router.frontend_routing(context) return frontend.handle_backend_error(error) def _run_bound_endpoint(self, context, spec): """ :type context: satosa.context.Context :type spec: ((satosa.context.Context, Any) -> satosa.response.Response, Any) | (satosa.context.Context) -> satosa.response.Response :param context: The request context :param spec: bound endpoint function :return: response """ try: if isinstance(spec, tuple): return spec[0](context, *spec[1:]) else: return spec(context) except SATOSAAuthenticationError as error: error.error_id = uuid4().urn msg = "ERROR_ID [{err_id}]\nSTATE:\n{state}".format( err_id=error.error_id, state=json.dumps(error.state.state_dict, indent=4)) satosa_logging(LOGGER, logging.ERROR, msg, error.state, exc_info=True) return self._handle_satosa_authentication_error(error) def _load_state(self, context): """ Load a state to the context :type context: satosa.context.Context :param context: Session context """ try: state = cookie_to_state(context.cookie, self.config.COOKIE_STATE_NAME, self.config.STATE_ENCRYPTION_KEY) except SATOSAStateError: state = State() context.state = state def _save_state(self, resp, context): """ Saves a state from context to cookie :type resp: satosa.response.Response :type context: satosa.context.Context :param resp: The response :param context: Session context """ if context.state.should_delete(): # Save empty state with a max age of 0 cookie = state_to_cookie(State(), self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY, 0) else: cookie = state_to_cookie(context.state, self.config.COOKIE_STATE_NAME, "/", self.config.STATE_ENCRYPTION_KEY) if isinstance(resp, Response): resp.add_cookie(cookie) else: try: resp.headers.append(tuple(cookie.output().split(": ", 1))) except: satosa_logging( LOGGER, logging.WARN, "can't add cookie to response '%s'" % resp.__class__, context.state) pass def run(self, context): """ Runs the satosa proxy with the given context. :type context: satosa.context.Context :rtype: satosa.response.Response :param context: The request context :return: response """ try: self._load_state(context) spec = self.module_router.endpoint_routing(context) resp = self._run_bound_endpoint(context, spec) self._save_state(resp, context) except SATOSAError: satosa_logging(LOGGER, logging.ERROR, "Uncaught SATOSA error", context.state, exc_info=True) raise except Exception as err: satosa_logging(LOGGER, logging.ERROR, "Uncaught exception", context.state, exc_info=True) raise SATOSAUnknownError("Unknown error") from err return resp