Mercurial > p > roundup > code
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, |
