Mercurial > p > roundup > code
view test/test_demo.py @ 8265:35beff316883
fix(api): issue2551384. Verify REST authorization earlier
To reduce the ability of bad actors to spam (DOS) the REST endpoint
with bad data and generate logs meant for debugging, modify the flow
in client.py's REST handler to verify authorization earlier.
If the anonymous user is allowed to use REST, this won't make a
difference for a DOS attempt. The templates don't enable REST for the
anonymous user by default. Most admins don't change this.
The validation order for REST requests has been changed.
CORS identfied an handled
User authorization to use REST (return 403 on failure)
REST request validated (Origin header valid etc.) (return 400 for
bad request)
Incorrectly formatted CORS preflight requests (e.g. missing Origin
header) that are not recogized as a CORS request can now return HTTP
status 403 as well as status 400 (when anonymous is allowed
access). Note all CORS preflights are sent without authentication so
appear as anonymous requests.
The tests were updated to compensate, but it is not obvious to me from
specs what the proper evaulation order/return codes should be for this
case. Both 403/400 are failures and cause CORS to fail so there should
be no difference but...
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 09 Jan 2025 09:30:08 -0500 |
| parents | 618dccf7199d |
| children |
line wrap: on
line source
import pytest import unittest import os, sys, shutil from os.path import normpath from roundup.demo import install_demo, run_demo import roundup.scripts.roundup_server # https://stackoverflow.com/questions/4219717/how-to-assert-output-with-nosetest-unittest-in-python # lightly modified from contextlib import contextmanager _py3 = sys.version_info[0] > 2 if _py3: from io import StringIO # py3 else: from StringIO import StringIO # py2 @contextmanager def captured_output(): new_out, new_err = StringIO(), StringIO() old_out, old_err = sys.stdout, sys.stderr try: sys.stdout, sys.stderr = new_out, new_err yield sys.stdout, sys.stderr finally: sys.stdout, sys.stderr = old_out, old_err try: import jinja2 skip_jinja2 = lambda func, *args, **kwargs: func except ImportError: from .pytest_patcher import mark_class skip_jinja2 = mark_class(pytest.mark.skip( reason='Skipping Jinja2 tests: jinja2 library not available')) class TestDemo(unittest.TestCase): def setUp(self): self.home = os.path.abspath('_test_demo') def tearDown(self): try: shutil.rmtree(self.home) except FileNotFoundError: pass def run_install_demo(self, template, db="anydbm"): with captured_output() as (out, err): install_demo(self.home, db, template) output = out.getvalue().strip() print(output) # verify that db was set properly by reading config with open(self.home + "/config.ini", "r") as f: config_lines = f.read().replace("\r\n", "\n") try: # handle text files with \r\n line endings config_lines.index("\r", 0, 100) config_lines = config_lines.replace("\r\n", "\n") except ValueError: pass self.assertIn("backend = %s\n"%db, config_lines) # dummy up the return of get_server so the serve_forever method # raises keyboard interrupt exiting the server so the test exits. gs = roundup.scripts.roundup_server.ServerConfig.get_server def raise_KeyboardInterrupt(): raise KeyboardInterrupt def test_get_server(self): httpd = gs(self) httpd.serve_forever = raise_KeyboardInterrupt return httpd roundup.scripts.roundup_server.ServerConfig.get_server = test_get_server # Run under context manager to capture output of startup text. with captured_output() as (out, err): run_demo(self.home) output = out.getvalue().strip() print(output) # if the server installed and started this will be the # last line in the output. self.assertIn("Keyboard Interrupt: exiting", output.split('\n')) def testDemoClassic(self): with captured_output() as (out, err): self.run_install_demo("classic") self.assertIn("http://localhost:8917/demo/", out.getvalue()) # verify the default anydbm db is created db_file = self.home + normpath("/db/nodes.user") db_file2 = db_file + '.db' db_file_dumbdbm = self.home + normpath("/db/nodes.user.dir") self.assertTrue(os.path.isfile(db_file) or os.path.isfile(db_file2) or os.path.isfile(db_file_dumbdbm), "expected db file %s[.db] or %s does not exist" % ( db_file, db_file_dumbdbm )) # verify requested template was used with open(self.home + "/TEMPLATE-INFO.txt", "r") as f: info_lines = f.read() try: # handle text files with \r\n line endings info_lines.index("\r", 0, 100) info_lines = info_lines.replace("\r\n", "\n") except ValueError: pass self.assertIn("Name: classic-_test_demo\n", info_lines) def testDemoMinimal(self): # test explicit path to template as others test template # search path. with captured_output() as (out, err): self.run_install_demo('../templates/minimal', db="sqlite") self.assertIn("http://localhost:8917/demo/", out.getvalue()) # verify the requested sqlite db file is created db_file = self.home + "/db/db" self.assertTrue(os.path.isfile(db_file), "expected db file %s does not exist" % db_file) # verify requested template was used with open(self.home + "/TEMPLATE-INFO.txt", "r") as f: info_lines = f.read() try: # handle text files with \r\n line endings info_lines.index("\r", 0, 100) info_lines = info_lines.replace("\r\n", "\n") except ValueError: pass self.assertIn("Name: minimal-_test_demo\n", info_lines) @skip_jinja2 def testDemoJinja(self): with captured_output() as (out, err): self.run_install_demo('jinja2', db="anydbm") self.assertIn("http://localhost:8917/demo/", out.getvalue()) # verify the requested anydbm db file is created db_file = self.home + "/db/nodes.user" db_file2 = db_file + '.db' self.assertTrue(os.path.isfile(db_file) or os.path.isfile(db_file2), "expected db file %s[.db] does not exist" % db_file) # verify that template was set to jinja2 by reading config with open(self.home + "/config.ini", "r") as f: config_lines = f.read() try: # handle text files with \r\n line endings config_lines.index("\r", 0, 100) config_lines = config_lines.replace("\r\n", "\n") except ValueError: pass self.assertIn("template_engine = jinja2\n", config_lines)
