comparison test/test_cgi.py @ 6693:9a1f5e496e6c

issue2551203 - Add support for CORS preflight request Add support for unauthenticated CORS preflight and fix headers for CORS. client.py: pass through unauthenticated CORS preflight to rest backend. Normal rest OPTION handlers (including tracker defined extensions) can see and handle the request. make some error cases return error json with crrect mime type rather than plain text tracebacks. create new functions to verify origin and referer that filter using allowed origins setting. remove tracker base url from error message is referer is not at an allowed origin. rest.py: fix up OPTION methods handlers to include Access-Control-Allow-Methods that are the same as the Allow header. set cache to one week for all Access-Control headers for CORS preflight only. remove self.client.setHeader("Access-Control-Allow-Origin", "*") and set Access-Control-Allow-Origin to the client supplied origin if it passes allowed origin checks. Required for CORS otherwise data isn't available to caller. Set for all responses. set Vary header now includes Origin as responses can differ based on Origin for all responses. set Access-Control-Allow-Credentials to true on all responses. test_liveserver.py: run server with setting to enforce origin csrf header check run server with setting to enforce x-requested-with csrf header check run server with setting for allowed_api_origins requests now set required csrf headers test preflight request on collections check new headers and Origin is no longer '*' rewrite all compression checks to use a single method with argument to use different compression methods. Reduce a lot of code duplication and makes updating for new headers easier. test_cgi: test new error messages in client.py account for new headers test preflight and new code paths
author John Rouillard <rouilj@ieee.org>
date Tue, 07 Jun 2022 09:39:35 -0400
parents ab2ed11c021e
children 1181157d7cec
comparison
equal deleted inserted replaced
6692:1fbfb4a277d7 6693:9a1f5e496e6c
1167 print("result of subtest invalid origin:", out[0]) 1167 print("result of subtest invalid origin:", out[0])
1168 self.assertEqual(match_at, 36) 1168 self.assertEqual(match_at, 36)
1169 del(cl.env['HTTP_ORIGIN']) 1169 del(cl.env['HTTP_ORIGIN'])
1170 cl.db.config.WEB_ALLOWED_API_ORIGINS = "" 1170 cl.db.config.WEB_ALLOWED_API_ORIGINS = ""
1171 del(out[0]) 1171 del(out[0])
1172
1173 # test by setting allowed api origins to *
1174 # this should not redirect as it is not an API call.
1175 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * "
1176 cl.env['HTTP_ORIGIN'] = 'http://whoami.com'
1177 cl.env['HTTP_REFERER'] = 'https://baz.edu/path/'
1178 cl.inner_main()
1179 match_at=out[0].find('Invalid Referer: https://baz.edu/path/')
1180 print("result of subtest invalid referer:", out[0])
1181 self.assertEqual(match_at, 36)
1182 del(cl.env['HTTP_ORIGIN'])
1183 del(cl.env['HTTP_REFERER'])
1184 cl.db.config.WEB_ALLOWED_API_ORIGINS = ""
1185 del(out[0])
1172 1186
1173 # clean up from email log 1187 # clean up from email log
1174 if os.path.exists(SENDMAILDEBUG): 1188 if os.path.exists(SENDMAILDEBUG):
1175 os.remove(SENDMAILDEBUG) 1189 os.remove(SENDMAILDEBUG)
1176 #raise ValueError 1190 #raise ValueError
1213 cl.write = wh # capture output 1227 cl.write = wh # capture output
1214 1228
1215 # Should return explanation because content type is text/plain 1229 # Should return explanation because content type is text/plain
1216 # and not text/xml 1230 # and not text/xml
1217 cl.handle_rest() 1231 cl.handle_rest()
1218 self.assertEqual(b2s(out[0]), "<class 'roundup.exceptions.UsageError'>: Required Header Missing\n") 1232 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Required Header Missing"}}')
1219 del(out[0]) 1233 del(out[0])
1220 1234
1221 cl = client.Client(self.instance, None, 1235 cl = client.Client(self.instance, None,
1222 {'REQUEST_METHOD':'POST', 1236 {'REQUEST_METHOD':'POST',
1223 'PATH_INFO':'rest/data/issue', 1237 'PATH_INFO':'rest/data/issue',
1318 cl.write = wh # capture output 1332 cl.write = wh # capture output
1319 1333
1320 # Should return explanation because content type is text/plain 1334 # Should return explanation because content type is text/plain
1321 # and not text/xml 1335 # and not text/xml
1322 cl.handle_rest() 1336 cl.handle_rest()
1323 self.assertEqual(b2s(out[0]), "<class 'roundup.exceptions.Unauthorised'>: Invalid Origin httxs://bar.edu\n") 1337 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Invalid Origin httxs://bar.edu"}}')
1324 del(out[0]) 1338 del(out[0])
1325 1339
1326 1340
1327 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * " 1341 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * "
1328 cl = client.Client(self.instance, None, 1342 cl = client.Client(self.instance, None,
1330 'PATH_INFO':'rest/data/issue', 1344 'PATH_INFO':'rest/data/issue',
1331 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 1345 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
1332 'HTTP_ORIGIN': 'httxs://bar.edu', 1346 'HTTP_ORIGIN': 'httxs://bar.edu',
1333 'HTTP_X_REQUESTED_WITH': 'rest', 1347 'HTTP_X_REQUESTED_WITH': 'rest',
1334 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', 1348 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1335 'HTTP_REFERER': 'http://whoami.com/path/', 1349 'HTTP_REFERER': 'httxp://bar.edu/path/',
1336 'HTTP_ACCEPT': "application/json;version=1" 1350 'HTTP_ACCEPT': "application/json;version=1"
1337 }, form) 1351 }, form)
1338 cl.db = self.db 1352 cl.db = self.db
1339 cl.base = 'http://whoami.com/path/' 1353 cl.base = 'http://whoami.com/path/'
1340 cl._socket_op = lambda *x : True 1354 cl._socket_op = lambda *x : True
1344 'accept': 'application/json' } 1358 'accept': 'application/json' }
1345 cl.request.headers = MockNull(**h) 1359 cl.request.headers = MockNull(**h)
1346 1360
1347 cl.write = wh # capture output 1361 cl.write = wh # capture output
1348 1362
1349 # create third issue 1363 # create fourth issue
1350 cl.handle_rest() 1364 cl.handle_rest()
1351 self.assertIn('"id": "3"', b2s(out[0])) 1365 self.assertIn('"id": "3"', b2s(out[0]))
1366 del(out[0])
1367
1368 cl.db.config.WEB_ALLOWED_API_ORIGINS = "httxs://bar.foo.edu httxs://bar.edu"
1369 for referer in [ 'httxs://bar.edu/path/foo',
1370 'httxs://bar.edu/path/foo?g=zz',
1371 'httxs://bar.edu']:
1372 cl = client.Client(self.instance, None,
1373 {'REQUEST_METHOD':'POST',
1374 'PATH_INFO':'rest/data/issue',
1375 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
1376 'HTTP_ORIGIN': 'httxs://bar.edu',
1377 'HTTP_X_REQUESTED_WITH': 'rest',
1378 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1379 'HTTP_REFERER': referer,
1380 'HTTP_ACCEPT': "application/json;version=1"
1381 }, form)
1382 cl.db = self.db
1383 cl.base = 'http://whoami.com/path/'
1384 cl._socket_op = lambda *x : True
1385 cl._error_message = []
1386 cl.request = MockNull()
1387 h = { 'content-type': 'application/json',
1388 'accept': 'application/json' }
1389 cl.request.headers = MockNull(**h)
1390
1391 cl.write = wh # capture output
1392
1393 # create fourth issue
1394 cl.handle_rest()
1395 self.assertIn('"id": "', b2s(out[0]))
1396 del(out[0])
1397
1398 cl.db.config.WEB_ALLOWED_API_ORIGINS = "httxs://bar.foo.edu httxs://bar.edu"
1399 cl = client.Client(self.instance, None,
1400 {'REQUEST_METHOD':'POST',
1401 'PATH_INFO':'rest/data/issue',
1402 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
1403 'HTTP_ORIGIN': 'httxs://bar.edu',
1404 'HTTP_X_REQUESTED_WITH': 'rest',
1405 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1406 'HTTP_REFERER': 'httxp://bar.edu/path/',
1407 'HTTP_ACCEPT': "application/json;version=1"
1408 }, form)
1409 cl.db = self.db
1410 cl.base = 'http://whoami.com/path/'
1411 cl._socket_op = lambda *x : True
1412 cl._error_message = []
1413 cl.request = MockNull()
1414 h = { 'content-type': 'application/json',
1415 'accept': 'application/json' }
1416 cl.request.headers = MockNull(**h)
1417
1418 cl.write = wh # capture output
1419
1420 # create fourth issue
1421 cl.handle_rest()
1422 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Invalid Referer: httxp://bar.edu/path/"}}')
1352 del(out[0]) 1423 del(out[0])
1353 1424
1354 def testXmlrpcCsrfProtection(self): 1425 def testXmlrpcCsrfProtection(self):
1355 # set the password for admin so we can log in. 1426 # set the password for admin so we can log in.
1356 passwd=password.Password('admin') 1427 passwd=password.Password('admin')

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