diff test/test_cgi.py @ 7153:1181157d7cec

Refactor rejecting requests; update tests, xfail test Added new Client::reject_request method. Deployed throughout handle_rest() method. Fix tests to compensate for consistent formatting of errors. Mark testRestOriginValidation test xfail. Code needed to implement it fully is only partly written. Tests for OPTIONS request on a bad attribute and valid and invalid origin tests added.
author John Rouillard <rouilj@ieee.org>
date Tue, 21 Feb 2023 22:35:58 -0500
parents 9a1f5e496e6c
children f614176903d0
line wrap: on
line diff
--- a/test/test_cgi.py	Tue Feb 21 18:54:21 2023 -0500
+++ b/test/test_cgi.py	Tue Feb 21 22:35:58 2023 -0500
@@ -1013,7 +1013,8 @@
         e2 = HTMLProperty.is_edit_ok
         HTMLProperty.is_edit_ok = lambda x : True
 
-        # test with no headers and config by default requires 1 
+        # test with no headers. Default config requires that 1 header
+        # is present and passes checks.
         cl.inner_main()
         match_at=out[0].find('Unable to verify sufficient headers')
         print("result of subtest 1:", out[0])
@@ -1189,6 +1190,306 @@
             os.remove(SENDMAILDEBUG)
         #raise ValueError
 
+    @pytest.mark.xfail
+    def testRestOriginValidation(self):
+        import json
+        # set the password for admin so we can log in.
+        passwd=password.Password('admin')
+        self.db.user.set('1', password=passwd)
+
+        out = []
+        def wh(s):
+           out.append(s)
+
+        # rest has no form content
+        form = cgi.FieldStorage()
+        # origin set to allowed value
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'GET',
+                            'PATH_INFO':'rest/data/issue',
+                            'HTTP_ORIGIN': 'http://whoami.com',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1",
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                        }, 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;version=1',
+            'origin': 'http://whoami.com',
+        }
+        cl.request.headers = MockNull(**h)
+                                      
+        cl.write = wh # capture output
+
+        cl.handle_rest()
+        print(b2s(out[0]))
+        expected="""
+        {
+            "data": {
+                "collection": [],
+                "@total_size": 0
+            }
+        }"""
+
+        self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
+        self.assertIn('Access-Control-Allow-Credentials',
+                      cl.additional_headers)
+        self.assertEqual(
+            cl.additional_headers['Access-Control-Allow-Credentials'],
+            'true'
+        )
+        self.assertEqual(
+            cl.additional_headers['Access-Control-Allow-Origin'],
+            'http://whoami.com'
+        )
+        del(out[0])
+
+
+        # origin not set to allowed value
+        # prevents authenticated request like this from
+        # being shared with the requestor because
+        # Access-Control-Allow-Credentials is not
+        # set in response
+        cl.db.config.WEB_ALLOWED_API_ORIGINS = " *  "
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'GET',
+                            'PATH_INFO':'rest/data/issue',
+                            'HTTP_ORIGIN': 'http://invalid.com',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://invalid.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1",
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                        }, 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;version=1',
+            'origin': 'http://invalid.com',
+        }
+        cl.request.headers = MockNull(**h)
+                                      
+        cl.write = wh # capture output
+        cl.handle_rest()
+        self.assertEqual(json.loads(b2s(out[0])),
+                         json.loads(expected)
+        )
+        self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
+        self.assertIn('Content-Length', cl.additional_headers)
+        del(out[0])
+
+
+        # origin not set. Same rules as for invalid origin
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'GET',
+                            'PATH_INFO':'rest/data/issue',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1",
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                        }, 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.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
+
+        self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
+        del(out[0])
+
+        # origin set to special "null" value. Same rules as for invalid origin
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'GET',
+                            'PATH_INFO':'rest/data/issue',
+                            'ORIGIN': 'null',
+                            'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'HTTP_ACCEPT': "application/json;version=1",
+                            'HTTP_X_REQUESTED_WITH': 'rest',
+                        }, 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',
+              'origin': 'null' }
+        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.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
+
+        self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
+        del(out[0])
+
+
+    def testRestOptionsBadAttribute(self):
+        out = []
+        def wh(s):
+            out.append(s)
+
+        # rest has no form content
+        form = cgi.FieldStorage()
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'OPTIONS',
+                            'HTTP_ORIGIN': 'http://whoami.com',
+                            'PATH_INFO':'rest/data/user/1/zot',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'content-type': ""
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = {
+            'origin': 'http://whoami.com',
+            'access-control-request-headers': 'x-requested-with',
+            'access-control-request-method': 'GET',
+            'referer': 'http://whoami.com/path',
+            'content-type': "",
+        }
+        cl.request.headers = MockNull(**h)
+
+        cl.write = wh # capture output
+        cl.handle_rest()
+
+        expected_headers = {
+            'Access-Control-Allow-Credentials': 'true',
+            'Access-Control-Allow-Headers': 'Content-Type, Authorization, '
+                                 'X-Requested-With, X-HTTP-Method-Override',
+            'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
+            'Access-Control-Allow-Origin': 'http://whoami.com',
+            'Access-Control-Max-Age': '86400',
+            'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
+            'Content-Length': '104',
+            'Content-Type': 'application/json',
+            'Vary': 'Origin'
+        }
+
+        expected_body = b'{\n    "error": {\n        "status": 404,\n        "msg": "Attribute zot not valid for Class user"\n    }\n}\n'
+
+        self.assertEqual(cl.response_code, 404)
+        self.assertEqual(out[0], expected_body)
+        self.assertEqual(cl.additional_headers, expected_headers)
+
+        del(out[0])
+
+
+    def testRestOptionsRequestGood(self):
+        import json
+        out = []
+        def wh(s):
+            out.append(s)
+
+        # OPTIONS/CORS preflight has no credentials
+        # rest has no form content
+        form = cgi.FieldStorage()
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'OPTIONS',
+                            'HTTP_ORIGIN': 'http://whoami.com',
+                            'PATH_INFO':'rest/data/issue',
+                            'HTTP_REFERER': 'http://whoami.com/path/',
+                            'Access-Control-Request-Headers': 'Authorization',
+                            'Access-Control-Request-Method': 'POST',
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = {
+            'origin': 'http://whoami.com',
+            'access-control-request-headers': 'Authorization',
+            'access-control-request-method': 'POST',
+            'referer': 'http://whoami.com/path',
+        }
+        cl.request.headers = MockNull(**h)
+
+        cl.write = wh # capture output
+        cl.handle_rest()
+        self.assertEqual(out[0], '')  # 204 options returns no data
+
+        expected_headers = {
+            'Access-Control-Allow-Credentials': 'true',
+            'Access-Control-Allow-Headers': 'Content-Type, Authorization, '
+                                 'X-Requested-With, X-HTTP-Method-Override',
+            'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
+            'Access-Control-Allow-Origin': 'http://whoami.com',
+            'Access-Control-Max-Age': '86400',
+            'Allow': 'OPTIONS, GET, POST',
+            'Content-Type': 'application/json',
+            'Vary': 'Origin'
+        }
+
+        self.assertEqual(cl.additional_headers, expected_headers)
+
+
+        del(out[0])
+
+    def testRestOptionsRequestBad(self):
+        import json
+
+        out = []
+        def wh(s):
+            out.append(s)
+
+        # OPTIONS/CORS preflight has no credentials
+        # rest has no form content
+        form = cgi.FieldStorage()
+        cl = client.Client(self.instance, None,
+                           {'REQUEST_METHOD':'OPTIONS',
+                            'HTTP_ORIGIN': 'http://invalid.com',
+                            'PATH_INFO':'rest/data/issue',
+                            'HTTP_REFERER':
+                            'http://invalid.com/path/',
+                            'Access-Control-Request-Headers': 'Authorization',
+                            'Access-Control-Request-Method': 'POST',
+                        }, form)
+        cl.db = self.db
+        cl.base = 'http://whoami.com/path/'
+        cl._socket_op = lambda *x : True
+        cl._error_message = []
+        cl.request = MockNull()
+        h = {
+            'origin': 'http://invalid.com',
+            'access-control-request-headers': 'Authorization',
+            'access-control-request-method': 'POST',
+            'referer': 'http://invalid.com/path',
+        }
+        cl.request.headers = MockNull(**h)
+
+        cl.write = wh # capture output
+        cl.handle_rest()
+
+        self.assertEqual(cl.response_code, 400)
+
+        del(out[0])
+
     def testRestCsrfProtection(self):
         import json
         # set the password for admin so we can log in.
@@ -1221,7 +1522,7 @@
         cl._error_message = []
         cl.request = MockNull()
         h = { 'content-type': 'application/json',
-              'accept': 'application/json' }
+              'accept': 'application/json;version=1' }
         cl.request.headers = MockNull(**h)
                                       
         cl.write = wh # capture output
@@ -1229,7 +1530,8 @@
         # Should return explanation because content type is text/plain
         # and not text/xml
         cl.handle_rest()
-        self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Required Header Missing"}}')
+        self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, '
+                         '"msg": "Required Header Missing" } }')
         del(out[0])
 
         cl = client.Client(self.instance, None,
@@ -1239,7 +1541,8 @@
                             'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
                             'HTTP_REFERER': 'http://whoami.com/path/',
                             'HTTP_X_REQUESTED_WITH': 'rest',
-                            'HTTP_ACCEPT': "application/json;version=1"
+                            'HTTP_ACCEPT': "application/json;version=1",
+                            'HTTP_ORIGIN': 'http://whoami.com',
                         }, form)
         cl.db = self.db
         cl.base = 'http://whoami.com/path/'
@@ -1334,7 +1637,7 @@
         # Should return explanation because content type is text/plain
         # and not text/xml
         cl.handle_rest()
-        self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Invalid Origin httxs://bar.edu"}}')
+        self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Client is not allowed to use Rest Interface." } }')
         del(out[0])
 
 

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