def test_no_default_exception_view(self) -> None: """Test Response Validation Error without default exception view. This causes the ResponseValidationError to bubble up to the top, because there is no view to render the HTTPException into a response. """ from pyramid.httpexceptions import HTTPPreconditionFailed from pyramid_openapi3.exceptions import ResponseValidationError # return the exception, so that response validation can kick in self._add_view(lambda *arg: HTTPPreconditionFailed()) # run request through router router = Router(self.config.registry) environ = { "wsgi.url_scheme": "http", "SERVER_NAME": "localhost", "SERVER_PORT": "8080", "REQUEST_METHOD": "GET", "PATH_INFO": "/foo", "QUERY_STRING": "bar=1", } start_response = DummyStartResponse() with self.assertRaises(ResponseValidationError) as cm: router(environ, start_response) self.assertEqual(cm.exception.status_code, 500) self.assertEqual("Unknown response http status: 412", str(cm.exception))
def test_response_validation_error(self) -> None: """Test View raises ResponseValidationError. Example view raises an undefined response code. The response validation tween should catch this as response validation error, and return an error 500. """ from pyramid.httpexceptions import HTTPPreconditionFailed self._add_view(lambda *arg: (_ for _ in ()).throw(HTTPPreconditionFailed())) self._add_default_exception_view() # run request through router router = Router(self.config.registry) environ = { "wsgi.url_scheme": "http", "SERVER_NAME": "localhost", "SERVER_PORT": "8080", "REQUEST_METHOD": "GET", "PATH_INFO": "/foo", "HTTP_ACCEPT": "application/json", "QUERY_STRING": "bar=1", } start_response = DummyStartResponse() response = router(environ, start_response) self.assertEqual(start_response.status, "500 Internal Server Error") self.assertIn( "Unknown response http status: 412", json.loads(b"".join(response))["message"], )
def check_precondition_headers(viewfunc, request): """View decorator to check X-If-[Unm|M]odified-Since headers. This decorator checks pre-validated vlaues from the X-If-Modified-Since and X-If-Unmodified-Since headers against the actual last-modified time of the target resource. If the preconditions are not met then it raises the appropriate error response. In addition, any retreived value for the last-modified time will be stored in the response headers for return to the client. This may save having to look it up again when the response is being rendered. """ if "if_modified_since" in request.validated: ts = get_resource_timestamp(request) request.response.headers["X-Last-Modified"] = str(ts) if ts <= request.validated["if_modified_since"]: raise HTTPNotModified(headers={ "X-Last-Modified": str(ts), }) if "if_unmodified_since" in request.validated: ts = get_resource_timestamp(request) request.response.headers["X-Last-Modified"] = str(ts) if ts > request.validated["if_unmodified_since"]: raise HTTPPreconditionFailed(headers={ "X-Last-Modified": str(ts), }) return viewfunc(request)
def test_process_conditional_put_requests(web_app, pyramid_request, etag, if_match, if_none_match, status_code): root = pyramid_request.root root['resource'] = resource = DummyResource({'foo': 'Hello', 'bar': 123}) resource.etag = etag resource_url = pyramid_request.resource_url(root['resource']) headers = {} if if_match: headers['If-Match'] = if_match if if_none_match: headers['If-None-Match'] = if_none_match kwargs = {'headers': headers} if status_code == 304: kwargs['exception'] = HTTPNotModified() elif status_code == 412: kwargs['exception'] = HTTPPreconditionFailed( {'etag': None if etag is None else etag.serialize()}) else: kwargs['status'] = status_code new_params = {'foo': 'World', 'bar': 456} web_app.put_json(resource_url, params=new_params, **kwargs)
def _assert_put_and_patch(usage_examples: UsageExamples, web_app: WebApp): """Test PUT and PATH requests.""" info_name = usage_examples.__class__.__name__ test_params = [ ('PUT', usage_examples.put_requests), ('PATCH', usage_examples.patch_requests), ] for http_method, examples_method in test_params: send = PutPatchRequestsTester(web_app, usage_examples, http_method.lower()) if examples_method: with usage_examples.send_function(send): examples_method() if http_method not in usage_examples.allowed_methods: assert send.calls_count == 0, '{} sends {} requests to resource'.format( info_name, http_method) continue assert send.calls_count > 0, '{} has not any {} requests'.format( info_name, http_method) etag = usage_examples.resource.get_etag() if etag: # if 'HEAD' in resource_examples.allowed_methods: # params, headers = resource_examples.authorize_request(None, None, None) # head_res = web_app.head(resource_examples.resource_url, params=params, headers=headers) # etag = head_res.headers['ETag'] resource = usage_examples.resource parent = resource.__parent__ if parent and 'GET' in usage_examples.allowed_methods: # Get a new resource instance with refreshed internal state etag = parent[resource.__name__].get_etag().serialize() else: # WARNING: This value of etag may be obsolete etag = etag.serialize() if not send.was_if_match: send( headers={'If-Match': '"__bad_etag__"'}, exception=HTTPPreconditionFailed({'etag': etag}), ) if not send.was_if_none_match: send( headers={'If-None-Match': etag}, exception=HTTPPreconditionFailed({'etag': etag}), )
def blob_transform_view(request): blobstore = request.repository.blob if not blobstore.blob_exists(request.context.model.id): raise HTTPPreconditionFailed('File is missing') blobstore.transform_blob(request.context) blobstore.finalize_blob(request.context) return request.context.model.to_dict()
def blob_upload_local_view(request): blobstore = request.repository.blob if blobstore.blob_exists(request.context.model.id): raise HTTPPreconditionFailed() blobstore.receive_blob(request, request.context) request.context.put() return BlobSchema().to_json(request.context.model.to_dict())
def blob_preview_local_view(request): # this is specific for the local implementation, preview_kind = request.context.model.info.get('preview_blob') if not preview_kind: raise HTTPPreconditionFailed('Preview is missing') request.repository.blob.backend.serve_preview_blob(request, request.response, request.context) return request.response
def blob_download_local_view(request): blobstore = request.repository.blob if not blobstore.blob_exists(request.context.model.id): raise HTTPPreconditionFailed('File is missing') response = request.response response.content_type = request.context.model.format response.content_length = request.context.model.bytes request.repository.blob.serve_blob(request, response, request.context) return response
def wrapped(context, request): if_match = str(request.if_match) if if_match == '*': return view_callable(context, request) uuid_tid = (v.split('=', 1) for v in if_match.strip('"').split('&')) root = request.root mismatching = (root.get_by_uuid(uuid).tid != UUID(tid) for uuid, tid in uuid_tid) if any(mismatching): raise HTTPPreconditionFailed("The resource has changed.") return view_callable(context, request)
def blob_transform_view(request): blob_key = request.matchdict['blob_key'] request.context.from_blob_key(blob_key) if request.context.model is None: raise HTTPNotFound() blobstore = request.repository.blob if not blobstore.blob_exists(blob_key): raise HTTPPreconditionFailed('File is missing') blobstore.transform_blob(request.context) return request.context.model.to_dict()
def _raise_412_if_modified(self, record=None): """Raise 412 if current timestamp is superior to the one specified in headers. :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` """ if_match = self.request.headers.get('If-Match') if_none_match = self.request.headers.get('If-None-Match') if not if_match and not if_none_match: return error_details = { 'location': 'header', 'description': ("Invalid value for If-Match. The value should " "be integer between double quotes.")} try: if_match = decode_header(if_match) if if_match else None if_none_match = decode_header(if_none_match) if if_none_match else None except UnicodeDecodeError: raise_invalid(self.request, **error_details) if record and if_none_match == '*': if record.get(self.model.deleted_field, False): # Tombstones should not prevent creation. return modified_since = -1 # Always raise. elif if_match: try: if not (if_match[0] == if_match[-1] == '"'): raise ValueError() modified_since = int(if_match[1:-1]) except (IndexError, ValueError): raise_invalid(self.request, **error_details) else: # In case _raise_304_if_not_modified() did not raise. return if record: current_timestamp = record[self.model.modified_field] else: current_timestamp = self.model.timestamp() if current_timestamp > modified_since: error_msg = 'Resource was modified meanwhile' details = {'existing': record} if record else {} response = http_error(HTTPPreconditionFailed(), errno=ERRORS.MODIFIED_MEANWHILE, message=error_msg, details=details) self._add_timestamp_header(response, timestamp=current_timestamp) raise response
def _raise_412_if_modified(self, obj=None): """Raise 412 if current timestamp is superior to the one specified in headers. :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` """ if_match = self.request.validated["header"].get("If-Match") if_none_match = self.request.validated["header"].get("If-None-Match") # Check if object exists object_exists = obj is not None # If no precondition headers, just ignore if not if_match and not if_none_match: return # If-None-Match: * should always raise if an object exists if if_none_match == "*" and object_exists: modified_since = -1 # Always raise. # If-Match should always raise if an object doesn't exist elif if_match and not object_exists: modified_since = -1 # If-Match with ETag value on existing objects should compare ETag elif if_match and if_match != "*": modified_since = if_match # If none of the above applies, don't raise else: return if obj: current_timestamp = obj[self.model.modified_field] else: current_timestamp = self.model.timestamp() if current_timestamp != modified_since: error_msg = "Resource was modified meanwhile" # Do not provide the permissions among the object fields. # Ref: https://github.com/Kinto/kinto/issues/224 existing = {**obj} if obj else {} existing.pop(self.model.permissions_field, None) details = {"existing": existing} if obj else {} response = http_error( HTTPPreconditionFailed(), errno=ERRORS.MODIFIED_MEANWHILE, message=error_msg, details=details, ) self._add_timestamp_header(response, timestamp=current_timestamp) raise response
def blob_upload_local_view(request): blob_key = request.matchdict['blob_key'] request.context.from_blob_key(request.matchdict['blob_key']) if request.context.model is None: raise HTTPNotFound() blobstore = request.repository.blob if blobstore.blob_exists(blob_key): raise HTTPPreconditionFailed() blobstore.receive_blob(request, request.context) return BlobSchema().to_json(request.context.model.to_dict())
def mapped_view(context, request: PyramidRequest): if context is not request.root: if_match = request.if_match if_none_match = request.if_none_match if if_match is not AnyETag or if_none_match is not NoETag: etag = context.get_etag() if etag is None: if None not in if_match: raise HTTPPreconditionFailed({'etag': None}) else: # https://tools.ietf.org/html/rfc7232#section-6 # https://tools.ietf.org/html/rfc7232#section-2.3.2 if if_match is not AnyETag: if not etag.is_strict or etag.value not in if_match: raise HTTPPreconditionFailed( {'etag': etag.serialize()}) if etag.value in if_none_match: if request.method in ('GET', 'HEAD'): raise HTTPNotModified() raise HTTPPreconditionFailed( {'etag': etag.serialize()}) return view(context, request)
def _raise_412_if_modified(self, record=None): """Raise 412 if current timestamp is superior to the one specified in headers. :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` """ if_match = self.request.validated["header"].get("If-Match") if_none_match = self.request.validated["header"].get("If-None-Match") # Check if record exists record_exists = record is not None # If no precondition headers, just ignore if not if_match and not if_none_match: return # If-None-Match: * should always raise if a record exists if if_none_match == "*" and record_exists: modified_since = -1 # Always raise. # If-Match should always raise if a record doesn't exist elif if_match and not record_exists: modified_since = -1 # If-Match with ETag value on existing records should compare ETag elif if_match and if_match != "*": modified_since = if_match # If none of the above applies, don't raise else: return if record: current_timestamp = record[self.model.modified_field] else: current_timestamp = self.model.timestamp() if current_timestamp != modified_since: error_msg = "Resource was modified meanwhile" details = {"existing": record} if record else {} response = http_error( HTTPPreconditionFailed(), errno=ERRORS.MODIFIED_MEANWHILE, message=error_msg, details=details, ) self._add_timestamp_header(response, timestamp=current_timestamp) raise response
def test_response_validation_error(self) -> None: """Test View raises ResponseValidationError. Example view raises an undefined response code. The response validation tween should catch this as response validation error, and return an error 500. """ from pyramid.httpexceptions import HTTPPreconditionFailed self._add_view(lambda *arg: HTTPPreconditionFailed()) # run request through router router = Router(self.config.registry) environ = { "wsgi.url_scheme": "http", "SERVER_NAME": "localhost", "SERVER_PORT": "8080", "REQUEST_METHOD": "GET", "PATH_INFO": "/foo", "HTTP_ACCEPT": "application/json", "QUERY_STRING": "bar=1", } start_response = DummyStartResponse() with self.assertLogs(level="ERROR") as cm: response = router(environ, start_response) self.assertEqual(start_response.status, "500 Internal Server Error") if openapi_core.__version__ == "0.13.8": # pragma: no cover self.assertEqual( json.loads(response[0]), [ { "exception": "ResponseNotFound", "message": "Unknown response http status: 412", } ], ) else: # pragma: no cover self.assertEqual( json.loads(response[0]), [ { "exception": "InvalidResponse", "message": "Unknown response http status: 412", } ], ) self.assertEqual( cm.output, ["ERROR:pyramid_openapi3:Unknown response http status: 412"] )
def delete_key(request): """Delete a key. You must have a valid session and be authenticated as the target user. """ appid = request.matchdict["appid"].encode("utf8") userid = request.matchdict["userid"].encode("utf8") key = request.matchdict["key"].encode("utf8") if_match = _get_if_match(request) store = request.registry.getUtility(ISauropodBackend) try: store.delete(appid, userid, key, if_match=if_match) except KeyError: raise HTTPNotFound() except ConflictError: raise HTTPPreconditionFailed() return HTTPNoContent()
def security_tween(request): login = None expected_user = request.headers.get('X-If-Match-User') if expected_user is not None: login = request.authenticated_userid if login != 'mailto.' + expected_user: detail = 'X-If-Match-User does not match' raise HTTPPreconditionFailed(detail) # wget may only send credentials following a challenge response. auth_challenge = asbool(request.headers.get('X-Auth-Challenge', False)) if auth_challenge or request.authorization is not None: login = request.authenticated_userid if login is None: raise HTTPUnauthorized(headerlist=forget(request)) if request.method in ('GET', 'HEAD'): return handler(request) if request.content_type != 'application/json': detail = "%s is not 'application/json'" % request.content_type raise HTTPUnsupportedMediaType(detail) token = request.headers.get('X-CSRF-Token') if token is not None: # Avoid dirtying the session and adding a Set-Cookie header # XXX Should consider if this is a good idea or not and timeouts if token == dict.get(request.session, '_csrft_', None): return handler(request) raise CSRFTokenError('Incorrect CSRF token') # NOTE: cutting out CSRF protection here ... why protect against CSRF if you provide an # unathenticated endpoint that will delivery the CSRF token? I'm looking at you /session. # this should be revisted, either embed the csrf token in the index.html as part of the # rendering subprocess somehow, or return it from the login view and let the client store it # but of course that sounds a lot like JWT... return handler(request) if login is None: login = request.authenticated_userid if login is not None: namespace, userid = login.split('.', 1) if namespace not in ('mailto', 'persona'): return handler(request) raise CSRFTokenError('Missing CSRF token')
def set_key(request): """Update the value of a key. You must have a valid session and be authenticated as the target user. """ appid = request.matchdict["appid"].encode("utf8") userid = request.matchdict["userid"].encode("utf8") key = request.matchdict["key"].encode("utf8") store = request.registry.getUtility(ISauropodBackend) value = request.POST.get("value") if value is None: raise HTTPBadRequest("mising value") if_match = _get_if_match(request) try: item = store.set(appid, userid, key, value, if_match=if_match) except ConflictError: raise HTTPPreconditionFailed() r = HTTPNoContent() if item.etag: r.headers["ETag"] = item.etag return r
def _assert_get_and_head(usage_examples: UsageExamples, web_app: WebApp): """Test GET requests.""" info_name = usage_examples.__class__.__name__ send = GetRequestsTester(web_app, usage_examples) if usage_examples.get_requests: with usage_examples.send_function(send): usage_examples.get_requests() if 'GET' not in usage_examples.allowed_methods: assert send.calls_count == 0, '{} sends GET requests to resource'.format( info_name) return assert send.calls_count > 0, '{} has not any GET requests'.format( info_name) etag = usage_examples.resource.get_etag() if etag: etag = etag.serialize() if not send.was_if_match: send( headers={'If-Match': '"__bad_etag__"'}, exception=HTTPPreconditionFailed({'etag': etag}), ) if not send.was_if_none_match: send( headers={'If-None-Match': etag}, exception=HTTPNotModified, ) # Test listing of embedded resources if (IHalResourceWithEmbeddedView.providedBy(usage_examples.view) and usage_examples.test_listing): orig_listing_conf = deepcopy(LISTING_CONF) try: assert_container_listing(usage_examples, web_app) finally: LISTING_CONF.clear() LISTING_CONF.update(orig_listing_conf)
def security_tween(request): login = None expected_user = request.headers.get('X-If-Match-User') if expected_user is not None: login = request.authenticated_userid if login != 'mailto.' + expected_user: detail = 'X-If-Match-User does not match' raise HTTPPreconditionFailed(detail) # wget may only send credentials following a challenge response. auth_challenge = asbool(request.headers.get('X-Auth-Challenge', False)) if auth_challenge or request.authorization is not None: login = request.authenticated_userid if login is None: raise HTTPUnauthorized(headerlist=forget(request)) if request.method in ('GET', 'HEAD'): return handler(request) if request.content_type != 'application/json': detail = "%s is not 'application/json'" % request.content_type raise HTTPUnsupportedMediaType(detail) token = request.headers.get('X-CSRF-Token') if token is not None: # Avoid dirtying the session and adding a Set-Cookie header # XXX Should consider if this is a good idea or not and timeouts if token == dict.get(request.session, '_csrft_', None): return handler(request) raise CSRFTokenError('Incorrect CSRF token') if login is None: login = request.authenticated_userid if login is not None: namespace, userid = login.split('.', 1) if namespace not in ('mailto', 'persona'): return handler(request) raise CSRFTokenError('Missing CSRF token')
def _raise_412_if_modified(self, record=None): """Raise 412 if current timestamp is superior to the one specified in headers. :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` """ if_match = self.request.validated['header'].get('If-Match') if_none_match = self.request.validated['header'].get('If-None-Match') if not if_match and not if_none_match: return if record and if_none_match == '*': if record.get(self.model.deleted_field, False): # Tombstones should not prevent creation. return modified_since = -1 # Always raise. elif if_match: modified_since = if_match else: # In case _raise_304_if_not_modified() did not raise. return if record: current_timestamp = record[self.model.modified_field] else: current_timestamp = self.model.timestamp() if current_timestamp > modified_since: error_msg = 'Resource was modified meanwhile' details = {'existing': record} if record else {} response = http_error(HTTPPreconditionFailed(), errno=ERRORS.MODIFIED_MEANWHILE, message=error_msg, details=details) self._add_timestamp_header(response, timestamp=current_timestamp) raise response
def _get_examples_info(self, usage_examples: UsageExamples, method: str) -> List[structs.ExampleInfo]: """Execute all examples of method and return list of ExampleInfo.""" send_requests = getattr(usage_examples, '%s_requests' % method, None) if send_requests is None: return [] send = _ExampleInfoCollector(self.web_app, usage_examples, method) with usage_examples.send_function(send): send_requests() if send.results and method in ('head', 'get'): etag = usage_examples.resource.get_etag() if etag: etag = etag.serialize() if all(not res.request_info.headers or 'If-Match' not in res.request_info.headers for res in send.results): send( headers={'If-Match': '"__bad_etag__"'}, exception=HTTPPreconditionFailed({'etag': etag}), ) if all(not res.request_info.headers or 'If-None-Match' not in res.request_info.headers for res in send.results): send( headers={'If-None-Match': etag}, exception=HTTPNotModified, ) elif send.results and method in ('put', 'patch'): etag = usage_examples.resource.get_etag() if etag: etag = etag.serialize() if all(not res.request_info.headers or 'If-Match' not in res.request_info.headers for res in send.results): send( headers={'If-Match': '"__bad_etag__"'}, exception=HTTPPreconditionFailed({'etag': ANY}), ) if all(not res.request_info.headers or 'If-None-Match' not in res.request_info.headers for res in send.results): # if 'HEAD' in usage_examples.allowed_methods: # params, headers = usage_examples.authorize_request(None, None, None) # head_res = self.web_app.head(usage_examples.resource_url, params=params, headers=headers) # etag = head_res.headers['ETag'] resource = usage_examples.resource parent = resource.__parent__ if parent and 'GET' in usage_examples.allowed_methods: # Get a new resource instance with refreshed internal state etag = parent[resource.__name__].get_etag().serialize() else: # WARNING: This value of etag may be obsolete etag = etag.serialize() send( headers={'If-None-Match': etag}, exception=HTTPPreconditionFailed({'etag': ANY}), ) return send.results
def server_id_response(request): request.response = HTTPPreconditionFailed() request.response.empty_body = True request.add_response_callback(server_id_callback) return request.response
def precondition_failed_redirect(request): ''' Errors get redirected here ''' return HTTPPreconditionFailed()
def auditstream_sse(self): """Returns an event stream suitable for driving an HTML5 EventSource. The event stream will contain auditing events. Obtain events for the context of the view only:: var source = new EventSource( "${request.sdiapi.mgmt_path(context, 'auditstream-sse')}"); Obtain events for a single OID unrelated to the context:: var source = new EventSource( "${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'oid':'12345'})}"); Obtain events for a set of OIDs:: var source = new EventSource( "${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'oid':['12345', '56789']})}"); Obtain all events for all oids:: var source = new EventSource( "${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'all':'1'})}"); The executing user will need to possess the ``sdi.view-auditstream`` permission against the context on which the view is invoked. """ request = self.request response = request.response response.content_type = 'text/event-stream' last_event_id = request.headers.get('Last-Event-Id') log = self.get_auditlog(self.context) if log is None: return HTTPPreconditionFailed('Auditing not configured') if not last_event_id: # first call, set a baseline event id gen, idx = log.latest_id() msg = compose_message('%s-%s' % (gen, idx)) response.text = msg self.logger.debug('New SSE connection on %s, returning %s' % (request.url, msg)) return response else: if request.GET.get('all'): oids = () elif request.GET.get('oid'): oids = map(int, request.GET.getall('oid')) else: oids = [get_oid(self.context)] _gen, _idx = map(int, last_event_id.split('-', 1)) events = log.newer(_gen, _idx, oids=oids) msg = text_type('') for gen, idx, event in events: event_id = '%s-%s' % (gen, idx) message = compose_message(event_id, event.name, event.payload) msg += message self.logger.debug( 'SSE connection on %s with id %s-%s, returning %s' % (request.url, _gen, _idx, msg)) response.text = msg return response