diff test/test_cgi.py @ 6681:ab2ed11c021e

issue2551205: Add support for specifying valid origins for api: xmlrpc/rest We now have an allow list to filter the hosts allowed to do api requests. An element of this allow list must match the http ORIGIN header exactly or the rest/xmlrpc CORS request will result in an error. The tracker host is always allowed to do a request.
author John Rouillard <rouilj@ieee.org>
date Tue, 17 May 2022 17:18:51 -0400
parents da6c9050a79e
children 9a1f5e496e6c
line wrap: on
line diff
--- a/test/test_cgi.py	Tue May 17 14:09:00 2022 -0400
+++ b/test/test_cgi.py	Tue May 17 17:18:51 2022 -0400
@@ -955,7 +955,7 @@
         # need to set SENDMAILDEBUG to prevent
         # downstream issue when email is sent on successful
         # issue creation. Also delete the file afterwards
-        # just tomake sure that someother test looking for
+        # just to make sure that some other test looking for
         # SENDMAILDEBUG won't trip over ours.
         if 'SENDMAILDEBUG' not in os.environ:
             os.environ['SENDMAILDEBUG'] = 'mail-test1.log'
@@ -1157,6 +1157,18 @@
         del(out[0])
 
         del(cl.env['HTTP_REFERER'])
+
+        # test by setting allowed api origins to *
+        # this should not redirect as it is not an API call.
+        cl.db.config.WEB_ALLOWED_API_ORIGINS = "  *  "
+        cl.env['HTTP_ORIGIN'] = 'https://baz.edu'
+        cl.inner_main()
+        match_at=out[0].find('Invalid Origin https://baz.edu')
+        print("result of subtest invalid origin:", out[0])
+        self.assertEqual(match_at, 36)
+        del(cl.env['HTTP_ORIGIN'])
+        cl.db.config.WEB_ALLOWED_API_ORIGINS = ""
+        del(out[0])
         
         # clean up from email log
         if os.path.exists(SENDMAILDEBUG):
@@ -1239,6 +1251,106 @@
         self.assertEqual(response,expected)
         del(out[0])
 
+
+        # rest has no form content
+        cl.db.config.WEB_ALLOWED_API_ORIGINS = "https://bar.edu http://bar.edu"
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('title', 'A new issue'),
+            cgi.MiniFieldStorage('status', '1'),
+            cgi.MiniFieldStorage('@pretty', 'false'),
+            cgi.MiniFieldStorage('@apiver', '1'),
+        ]
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'POST',
+                            'PATH_INFO':'rest/data/issue',
+                            'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+                            'HTTP_ORIGIN': 'https://bar.edu',
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1"
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = { 'content-type': 'application/json',
+              'accept': 'application/json' }
+        cl.request.headers = MockNull(**h)
+                                      
+        cl.write = wh # capture output
+
+        # Should return explanation because content type is text/plain
+        # and not text/xml
+        cl.handle_rest()
+        answer='{"data": {"link": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/issue/2", "id": "2"}}\n'
+        # check length to see if pretty is turned off.
+        self.assertEqual(len(out[0]), 99)
+
+        # compare as dicts not strings due to different key ordering
+        # between python versions.
+        response=json.loads(b2s(out[0]))
+        expected=json.loads(answer)
+        self.assertEqual(response,expected)
+        del(out[0])
+
+        #####
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'POST',
+                            'PATH_INFO':'rest/data/issue',
+                            'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+                            'HTTP_ORIGIN': 'httxs://bar.edu',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1"
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = { 'content-type': 'application/json',
+              'accept': 'application/json' }
+        cl.request.headers = MockNull(**h)
+                                      
+        cl.write = wh # capture output
+
+        # Should return explanation because content type is text/plain
+        # and not text/xml
+        cl.handle_rest()
+        self.assertEqual(b2s(out[0]), "<class 'roundup.exceptions.Unauthorised'>: Invalid Origin httxs://bar.edu\n")
+        del(out[0])
+
+
+        cl.db.config.WEB_ALLOWED_API_ORIGINS = "  * "
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'POST',
+                            'PATH_INFO':'rest/data/issue',
+                            'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+                            'HTTP_ORIGIN': 'httxs://bar.edu',
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1"
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = { 'content-type': 'application/json',
+              'accept': 'application/json' }
+        cl.request.headers = MockNull(**h)
+                                      
+        cl.write = wh # capture output
+
+        # create third issue
+        cl.handle_rest()
+        self.assertIn('"id": "3"', b2s(out[0]))
+        del(out[0])
+
     def testXmlrpcCsrfProtection(self):
         # set the password for admin so we can log in.
         passwd=password.Password('admin')
@@ -1663,12 +1775,11 @@
         # need to set SENDMAILDEBUG to prevent
         # downstream issue when email is sent on successful
         # issue creation. Also delete the file afterwards
-        # just tomake sure that someother test looking for
+        # just to make sure that some other test looking for
         # SENDMAILDEBUG won't trip over ours.
         if 'SENDMAILDEBUG' not in os.environ:
             os.environ['SENDMAILDEBUG'] = 'mail-test1.log'
         SENDMAILDEBUG = os.environ['SENDMAILDEBUG']
-
         
         # missing opaqueregister
         cl = self._make_client({'username':'new_user1', 'password':'secret',
@@ -1727,7 +1838,7 @@
         # need to set SENDMAILDEBUG to prevent
         # downstream issue when email is sent on successful
         # issue creation. Also delete the file afterwards
-        # just tomake sure that someother test looking for
+        # just to make sure that some other test looking for
         # SENDMAILDEBUG won't trip over ours.
         if 'SENDMAILDEBUG' not in os.environ:
             os.environ['SENDMAILDEBUG'] = 'mail-test1.log'

Roundup Issue Tracker: http://roundup-tracker.org/