comparison 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
comparison
equal deleted inserted replaced
7152:4e0665238617 7153:1181157d7cec
1011 e1 = _HTMLItem.is_edit_ok 1011 e1 = _HTMLItem.is_edit_ok
1012 _HTMLItem.is_edit_ok = lambda x : True 1012 _HTMLItem.is_edit_ok = lambda x : True
1013 e2 = HTMLProperty.is_edit_ok 1013 e2 = HTMLProperty.is_edit_ok
1014 HTMLProperty.is_edit_ok = lambda x : True 1014 HTMLProperty.is_edit_ok = lambda x : True
1015 1015
1016 # test with no headers and config by default requires 1 1016 # test with no headers. Default config requires that 1 header
1017 # is present and passes checks.
1017 cl.inner_main() 1018 cl.inner_main()
1018 match_at=out[0].find('Unable to verify sufficient headers') 1019 match_at=out[0].find('Unable to verify sufficient headers')
1019 print("result of subtest 1:", out[0]) 1020 print("result of subtest 1:", out[0])
1020 self.assertNotEqual(match_at, -1) 1021 self.assertNotEqual(match_at, -1)
1021 del(out[0]) 1022 del(out[0])
1186 1187
1187 # clean up from email log 1188 # clean up from email log
1188 if os.path.exists(SENDMAILDEBUG): 1189 if os.path.exists(SENDMAILDEBUG):
1189 os.remove(SENDMAILDEBUG) 1190 os.remove(SENDMAILDEBUG)
1190 #raise ValueError 1191 #raise ValueError
1192
1193 @pytest.mark.xfail
1194 def testRestOriginValidation(self):
1195 import json
1196 # set the password for admin so we can log in.
1197 passwd=password.Password('admin')
1198 self.db.user.set('1', password=passwd)
1199
1200 out = []
1201 def wh(s):
1202 out.append(s)
1203
1204 # rest has no form content
1205 form = cgi.FieldStorage()
1206 # origin set to allowed value
1207 cl = client.Client(self.instance, None,
1208 {'REQUEST_METHOD':'GET',
1209 'PATH_INFO':'rest/data/issue',
1210 'HTTP_ORIGIN': 'http://whoami.com',
1211 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1212 'HTTP_REFERER': 'http://whoami.com/path/',
1213 'HTTP_ACCEPT': "application/json;version=1",
1214 'HTTP_X_REQUESTED_WITH': 'rest',
1215 }, form)
1216 cl.db = self.db
1217 cl.base = 'http://whoami.com/path/'
1218 cl._socket_op = lambda *x : True
1219 cl._error_message = []
1220 cl.request = MockNull()
1221 h = {
1222 'content-type': 'application/json',
1223 'accept': 'application/json;version=1',
1224 'origin': 'http://whoami.com',
1225 }
1226 cl.request.headers = MockNull(**h)
1227
1228 cl.write = wh # capture output
1229
1230 cl.handle_rest()
1231 print(b2s(out[0]))
1232 expected="""
1233 {
1234 "data": {
1235 "collection": [],
1236 "@total_size": 0
1237 }
1238 }"""
1239
1240 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
1241 self.assertIn('Access-Control-Allow-Credentials',
1242 cl.additional_headers)
1243 self.assertEqual(
1244 cl.additional_headers['Access-Control-Allow-Credentials'],
1245 'true'
1246 )
1247 self.assertEqual(
1248 cl.additional_headers['Access-Control-Allow-Origin'],
1249 'http://whoami.com'
1250 )
1251 del(out[0])
1252
1253
1254 # origin not set to allowed value
1255 # prevents authenticated request like this from
1256 # being shared with the requestor because
1257 # Access-Control-Allow-Credentials is not
1258 # set in response
1259 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * "
1260 cl = client.Client(self.instance, None,
1261 {'REQUEST_METHOD':'GET',
1262 'PATH_INFO':'rest/data/issue',
1263 'HTTP_ORIGIN': 'http://invalid.com',
1264 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1265 'HTTP_REFERER': 'http://invalid.com/path/',
1266 'HTTP_ACCEPT': "application/json;version=1",
1267 'HTTP_X_REQUESTED_WITH': 'rest',
1268 }, form)
1269 cl.db = self.db
1270 cl.base = 'http://whoami.com/path/'
1271 cl._socket_op = lambda *x : True
1272 cl._error_message = []
1273 cl.request = MockNull()
1274 h = {
1275 'content-type': 'application/json',
1276 'accept': 'application/json;version=1',
1277 'origin': 'http://invalid.com',
1278 }
1279 cl.request.headers = MockNull(**h)
1280
1281 cl.write = wh # capture output
1282 cl.handle_rest()
1283 self.assertEqual(json.loads(b2s(out[0])),
1284 json.loads(expected)
1285 )
1286 self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
1287 self.assertIn('Content-Length', cl.additional_headers)
1288 del(out[0])
1289
1290
1291 # origin not set. Same rules as for invalid origin
1292 cl = client.Client(self.instance, None,
1293 {'REQUEST_METHOD':'GET',
1294 'PATH_INFO':'rest/data/issue',
1295 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1296 'HTTP_REFERER': 'http://whoami.com/path/',
1297 'HTTP_ACCEPT': "application/json;version=1",
1298 'HTTP_X_REQUESTED_WITH': 'rest',
1299 }, form)
1300 cl.db = self.db
1301 cl.base = 'http://whoami.com/path/'
1302 cl._socket_op = lambda *x : True
1303 cl._error_message = []
1304 cl.request = MockNull()
1305 h = { 'content-type': 'application/json',
1306 'accept': 'application/json' }
1307 cl.request.headers = MockNull(**h)
1308
1309 cl.write = wh # capture output
1310
1311 # Should return explanation because content type is text/plain
1312 # and not text/xml
1313 cl.handle_rest()
1314 self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
1315
1316 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
1317 del(out[0])
1318
1319 # origin set to special "null" value. Same rules as for invalid origin
1320 cl = client.Client(self.instance, None,
1321 {'REQUEST_METHOD':'GET',
1322 'PATH_INFO':'rest/data/issue',
1323 'ORIGIN': 'null',
1324 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1325 'HTTP_REFERER': 'http://whoami.com/path/',
1326 'HTTP_ACCEPT': "application/json;version=1",
1327 'HTTP_X_REQUESTED_WITH': 'rest',
1328 }, form)
1329 cl.db = self.db
1330 cl.base = 'http://whoami.com/path/'
1331 cl._socket_op = lambda *x : True
1332 cl._error_message = []
1333 cl.request = MockNull()
1334 h = { 'content-type': 'application/json',
1335 'accept': 'application/json',
1336 'origin': 'null' }
1337 cl.request.headers = MockNull(**h)
1338
1339 cl.write = wh # capture output
1340
1341 # Should return explanation because content type is text/plain
1342 # and not text/xml
1343 cl.handle_rest()
1344 self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers)
1345
1346 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected))
1347 del(out[0])
1348
1349
1350 def testRestOptionsBadAttribute(self):
1351 out = []
1352 def wh(s):
1353 out.append(s)
1354
1355 # rest has no form content
1356 form = cgi.FieldStorage()
1357 cl = client.Client(self.instance, None,
1358 {'REQUEST_METHOD':'OPTIONS',
1359 'HTTP_ORIGIN': 'http://whoami.com',
1360 'PATH_INFO':'rest/data/user/1/zot',
1361 'HTTP_REFERER': 'http://whoami.com/path/',
1362 'content-type': ""
1363 }, form)
1364 cl.db = self.db
1365 cl.base = 'http://whoami.com/path/'
1366 cl._socket_op = lambda *x : True
1367 cl._error_message = []
1368 cl.request = MockNull()
1369 h = {
1370 'origin': 'http://whoami.com',
1371 'access-control-request-headers': 'x-requested-with',
1372 'access-control-request-method': 'GET',
1373 'referer': 'http://whoami.com/path',
1374 'content-type': "",
1375 }
1376 cl.request.headers = MockNull(**h)
1377
1378 cl.write = wh # capture output
1379 cl.handle_rest()
1380
1381 expected_headers = {
1382 'Access-Control-Allow-Credentials': 'true',
1383 'Access-Control-Allow-Headers': 'Content-Type, Authorization, '
1384 'X-Requested-With, X-HTTP-Method-Override',
1385 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
1386 'Access-Control-Allow-Origin': 'http://whoami.com',
1387 'Access-Control-Max-Age': '86400',
1388 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
1389 'Content-Length': '104',
1390 'Content-Type': 'application/json',
1391 'Vary': 'Origin'
1392 }
1393
1394 expected_body = b'{\n "error": {\n "status": 404,\n "msg": "Attribute zot not valid for Class user"\n }\n}\n'
1395
1396 self.assertEqual(cl.response_code, 404)
1397 self.assertEqual(out[0], expected_body)
1398 self.assertEqual(cl.additional_headers, expected_headers)
1399
1400 del(out[0])
1401
1402
1403 def testRestOptionsRequestGood(self):
1404 import json
1405 out = []
1406 def wh(s):
1407 out.append(s)
1408
1409 # OPTIONS/CORS preflight has no credentials
1410 # rest has no form content
1411 form = cgi.FieldStorage()
1412 cl = client.Client(self.instance, None,
1413 {'REQUEST_METHOD':'OPTIONS',
1414 'HTTP_ORIGIN': 'http://whoami.com',
1415 'PATH_INFO':'rest/data/issue',
1416 'HTTP_REFERER': 'http://whoami.com/path/',
1417 'Access-Control-Request-Headers': 'Authorization',
1418 'Access-Control-Request-Method': 'POST',
1419 }, form)
1420 cl.db = self.db
1421 cl.base = 'http://whoami.com/path/'
1422 cl._socket_op = lambda *x : True
1423 cl._error_message = []
1424 cl.request = MockNull()
1425 h = {
1426 'origin': 'http://whoami.com',
1427 'access-control-request-headers': 'Authorization',
1428 'access-control-request-method': 'POST',
1429 'referer': 'http://whoami.com/path',
1430 }
1431 cl.request.headers = MockNull(**h)
1432
1433 cl.write = wh # capture output
1434 cl.handle_rest()
1435 self.assertEqual(out[0], '') # 204 options returns no data
1436
1437 expected_headers = {
1438 'Access-Control-Allow-Credentials': 'true',
1439 'Access-Control-Allow-Headers': 'Content-Type, Authorization, '
1440 'X-Requested-With, X-HTTP-Method-Override',
1441 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
1442 'Access-Control-Allow-Origin': 'http://whoami.com',
1443 'Access-Control-Max-Age': '86400',
1444 'Allow': 'OPTIONS, GET, POST',
1445 'Content-Type': 'application/json',
1446 'Vary': 'Origin'
1447 }
1448
1449 self.assertEqual(cl.additional_headers, expected_headers)
1450
1451
1452 del(out[0])
1453
1454 def testRestOptionsRequestBad(self):
1455 import json
1456
1457 out = []
1458 def wh(s):
1459 out.append(s)
1460
1461 # OPTIONS/CORS preflight has no credentials
1462 # rest has no form content
1463 form = cgi.FieldStorage()
1464 cl = client.Client(self.instance, None,
1465 {'REQUEST_METHOD':'OPTIONS',
1466 'HTTP_ORIGIN': 'http://invalid.com',
1467 'PATH_INFO':'rest/data/issue',
1468 'HTTP_REFERER':
1469 'http://invalid.com/path/',
1470 'Access-Control-Request-Headers': 'Authorization',
1471 'Access-Control-Request-Method': 'POST',
1472 }, form)
1473 cl.db = self.db
1474 cl.base = 'http://whoami.com/path/'
1475 cl._socket_op = lambda *x : True
1476 cl._error_message = []
1477 cl.request = MockNull()
1478 h = {
1479 'origin': 'http://invalid.com',
1480 'access-control-request-headers': 'Authorization',
1481 'access-control-request-method': 'POST',
1482 'referer': 'http://invalid.com/path',
1483 }
1484 cl.request.headers = MockNull(**h)
1485
1486 cl.write = wh # capture output
1487 cl.handle_rest()
1488
1489 self.assertEqual(cl.response_code, 400)
1490
1491 del(out[0])
1191 1492
1192 def testRestCsrfProtection(self): 1493 def testRestCsrfProtection(self):
1193 import json 1494 import json
1194 # set the password for admin so we can log in. 1495 # set the password for admin so we can log in.
1195 passwd=password.Password('admin') 1496 passwd=password.Password('admin')
1219 cl.base = 'http://whoami.com/path/' 1520 cl.base = 'http://whoami.com/path/'
1220 cl._socket_op = lambda *x : True 1521 cl._socket_op = lambda *x : True
1221 cl._error_message = [] 1522 cl._error_message = []
1222 cl.request = MockNull() 1523 cl.request = MockNull()
1223 h = { 'content-type': 'application/json', 1524 h = { 'content-type': 'application/json',
1224 'accept': 'application/json' } 1525 'accept': 'application/json;version=1' }
1225 cl.request.headers = MockNull(**h) 1526 cl.request.headers = MockNull(**h)
1226 1527
1227 cl.write = wh # capture output 1528 cl.write = wh # capture output
1228 1529
1229 # Should return explanation because content type is text/plain 1530 # Should return explanation because content type is text/plain
1230 # and not text/xml 1531 # and not text/xml
1231 cl.handle_rest() 1532 cl.handle_rest()
1232 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Required Header Missing"}}') 1533 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, '
1534 '"msg": "Required Header Missing" } }')
1233 del(out[0]) 1535 del(out[0])
1234 1536
1235 cl = client.Client(self.instance, None, 1537 cl = client.Client(self.instance, None,
1236 {'REQUEST_METHOD':'POST', 1538 {'REQUEST_METHOD':'POST',
1237 'PATH_INFO':'rest/data/issue', 1539 'PATH_INFO':'rest/data/issue',
1238 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 1540 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
1239 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', 1541 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1240 'HTTP_REFERER': 'http://whoami.com/path/', 1542 'HTTP_REFERER': 'http://whoami.com/path/',
1241 'HTTP_X_REQUESTED_WITH': 'rest', 1543 'HTTP_X_REQUESTED_WITH': 'rest',
1242 'HTTP_ACCEPT': "application/json;version=1" 1544 'HTTP_ACCEPT': "application/json;version=1",
1545 'HTTP_ORIGIN': 'http://whoami.com',
1243 }, form) 1546 }, form)
1244 cl.db = self.db 1547 cl.db = self.db
1245 cl.base = 'http://whoami.com/path/' 1548 cl.base = 'http://whoami.com/path/'
1246 cl._socket_op = lambda *x : True 1549 cl._socket_op = lambda *x : True
1247 cl._error_message = [] 1550 cl._error_message = []
1332 cl.write = wh # capture output 1635 cl.write = wh # capture output
1333 1636
1334 # Should return explanation because content type is text/plain 1637 # Should return explanation because content type is text/plain
1335 # and not text/xml 1638 # and not text/xml
1336 cl.handle_rest() 1639 cl.handle_rest()
1337 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Invalid Origin httxs://bar.edu"}}') 1640 self.assertEqual(b2s(out[0]), '{ "error": { "status": 400, "msg": "Client is not allowed to use Rest Interface." } }')
1338 del(out[0]) 1641 del(out[0])
1339 1642
1340 1643
1341 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * " 1644 cl.db.config.WEB_ALLOWED_API_ORIGINS = " * "
1342 cl = client.Client(self.instance, None, 1645 cl = client.Client(self.instance, None,

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