Mercurial > p > roundup > code
comparison test/rest_common.py @ 7155:89a59e46b3af
improve REST interface security
When using REST, we reflect the client's origin. If the wildcard '*'
is used in allowed_api_origins all origins are allowed. When this is
done, it also added an 'Access-Control-Allow-Credentials: true'
header.
This Credentials header should not be added if the site is matched
only by '*'. This header should be provided only for explicit origins
(e.g. https://example.org) not for the wildcard.
This is now fixed for CORS preflight OPTIONS request as well as normal
GET, PUT, DELETE, POST, PATCH and OPTIONS requests.
A missing Access-Control-Allow-Credentials will prevent the tracker
from being accessed using credentials. This prevents an unauthorized
third party web site from using a user's credentials to access
information in the tracker that is not publicly available.
Added test for this specific case.
In addition, allowed_api_origins can include explicit origins in
addition to '*'. '*' must be first in the list.
Also adapted numerous tests to work with these changes.
Doc updates.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 23 Feb 2023 12:01:33 -0500 |
| parents | 32c6e98e5a21 |
| children | 6f09103a6522 |
comparison
equal
deleted
inserted
replaced
| 7154:f614176903d0 | 7155:89a59e46b3af |
|---|---|
| 231 | 231 |
| 232 self.db.post_init() | 232 self.db.post_init() |
| 233 | 233 |
| 234 tx_Source_init(self.db) | 234 tx_Source_init(self.db) |
| 235 | 235 |
| 236 env = { | 236 self.client_env = { |
| 237 'PATH_INFO': 'http://localhost/rounduptest/rest/', | 237 'PATH_INFO': 'http://localhost/rounduptest/rest/', |
| 238 'HTTP_HOST': 'localhost', | 238 'HTTP_HOST': 'localhost', |
| 239 'TRACKER_NAME': 'rounduptest' | 239 'TRACKER_NAME': 'rounduptest', |
| 240 } | 240 'HTTP_ORIGIN': 'http://tracker.example' |
| 241 self.dummy_client = client.Client(self.instance, MockNull(), env, [], None) | 241 } |
| 242 self.dummy_client = client.Client(self.instance, MockNull(), | |
| 243 self.client_env, [], None) | |
| 242 self.dummy_client.request.headers.get = self.get_header | 244 self.dummy_client.request.headers.get = self.get_header |
| 245 self.dummy_client.db = self.db | |
| 246 | |
| 243 self.empty_form = cgi.FieldStorage() | 247 self.empty_form = cgi.FieldStorage() |
| 244 self.terse_form = cgi.FieldStorage() | 248 self.terse_form = cgi.FieldStorage() |
| 245 self.terse_form.list = [ | 249 self.terse_form.list = [ |
| 246 cgi.MiniFieldStorage('@verbose', '0'), | 250 cgi.MiniFieldStorage('@verbose', '0'), |
| 247 ] | 251 ] |
| 262 | 266 |
| 263 def get_header (self, header, not_found=None): | 267 def get_header (self, header, not_found=None): |
| 264 try: | 268 try: |
| 265 return self.headers[header.lower()] | 269 return self.headers[header.lower()] |
| 266 except (AttributeError, KeyError, TypeError): | 270 except (AttributeError, KeyError, TypeError): |
| 271 if header.upper() in self.client_env: | |
| 272 return self.client_env[header.upper()] | |
| 267 return not_found | 273 return not_found |
| 268 | 274 |
| 269 def create_stati(self): | 275 def create_stati(self): |
| 270 try: | 276 try: |
| 271 self.db.status.create(name='open', order='9') | 277 self.db.status.create(name='open', order='9') |
| 309 def testGet(self): | 315 def testGet(self): |
| 310 """ | 316 """ |
| 311 Retrieve all three users | 317 Retrieve all three users |
| 312 obtain data for 'joe' | 318 obtain data for 'joe' |
| 313 """ | 319 """ |
| 320 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 314 # Retrieve all three users. | 321 # Retrieve all three users. |
| 315 results = self.server.get_collection('user', self.empty_form) | 322 results = self.server.get_collection('user', self.empty_form) |
| 316 self.assertEqual(self.dummy_client.response_code, 200) | 323 self.assertEqual(self.dummy_client.response_code, 200) |
| 317 self.assertEqual(len(results['data']['collection']), 3) | 324 self.assertEqual(len(results['data']['collection']), 3) |
| 318 self.assertEqual(results['data']['@total_size'], 3) | 325 self.assertEqual(results['data']['@total_size'], 3) |
| 1080 # don't set an accept header; json should be the default | 1087 # don't set an accept header; json should be the default |
| 1081 # use up all our allowed api calls | 1088 # use up all our allowed api calls |
| 1082 for i in range(20): | 1089 for i in range(20): |
| 1083 # i is 0 ... 19 | 1090 # i is 0 ... 19 |
| 1084 self.client_error_message = [] | 1091 self.client_error_message = [] |
| 1092 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 1085 results = self.server.dispatch('GET', | 1093 results = self.server.dispatch('GET', |
| 1086 "/rest/data/user/%s/realname"%self.joeid, | 1094 "/rest/data/user/%s/realname"%self.joeid, |
| 1087 self.empty_form) | 1095 self.empty_form) |
| 1088 | 1096 |
| 1089 # is successful | 1097 # is successful |
| 1316 }' | 1324 }' |
| 1317 env = { "CONTENT_TYPE": "application/json", | 1325 env = { "CONTENT_TYPE": "application/json", |
| 1318 "CONTENT_LENGTH": len(body), | 1326 "CONTENT_LENGTH": len(body), |
| 1319 "REQUEST_METHOD": "POST" | 1327 "REQUEST_METHOD": "POST" |
| 1320 } | 1328 } |
| 1329 self.server.client.env.update(env) | |
| 1330 | |
| 1321 headers={"accept": "application/json; version=1", | 1331 headers={"accept": "application/json; version=1", |
| 1322 "content-type": env['CONTENT_TYPE'], | 1332 "content-type": env['CONTENT_TYPE'], |
| 1323 "content-length": env['CONTENT_LENGTH'], | 1333 "content-length": env['CONTENT_LENGTH'], |
| 1324 } | 1334 } |
| 1325 self.headers=headers | 1335 self.headers=headers |
| 1358 }' | 1368 }' |
| 1359 env = { "CONTENT_TYPE": "application/json", | 1369 env = { "CONTENT_TYPE": "application/json", |
| 1360 "CONTENT_LENGTH": len(body), | 1370 "CONTENT_LENGTH": len(body), |
| 1361 "REQUEST_METHOD": "POST" | 1371 "REQUEST_METHOD": "POST" |
| 1362 } | 1372 } |
| 1373 self.server.client.env.update(env) | |
| 1363 headers={"accept": "application/json; version=1", | 1374 headers={"accept": "application/json; version=1", |
| 1364 "content-type": env['CONTENT_TYPE'], | 1375 "content-type": env['CONTENT_TYPE'], |
| 1365 "content-length": env['CONTENT_LENGTH'], | 1376 "content-length": env['CONTENT_LENGTH'], |
| 1366 } | 1377 } |
| 1367 self.headers=headers | 1378 self.headers=headers |
| 1381 self.assertEqual(self.server.client.response_code, 201) | 1392 self.assertEqual(self.server.client.response_code, 201) |
| 1382 json_dict = json.loads(b2s(results)) | 1393 json_dict = json.loads(b2s(results)) |
| 1383 self.assertEqual(json_dict['data']['link'], | 1394 self.assertEqual(json_dict['data']['link'], |
| 1384 "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/issue/1") | 1395 "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/issue/1") |
| 1385 self.assertEqual(json_dict['data']['id'], "1") | 1396 self.assertEqual(json_dict['data']['id'], "1") |
| 1397 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 1386 results = self.server.dispatch('GET', | 1398 results = self.server.dispatch('GET', |
| 1387 "/rest/data/issue/1", self.empty_form) | 1399 "/rest/data/issue/1", self.empty_form) |
| 1388 print(results) | 1400 print(results) |
| 1389 json_dict = json.loads(b2s(results)) | 1401 json_dict = json.loads(b2s(results)) |
| 1390 self.assertEqual(json_dict['data']['link'], | 1402 self.assertEqual(json_dict['data']['link'], |
| 1406 # TEST #0 | 1418 # TEST #0 |
| 1407 # Delete class raises unauthorized error | 1419 # Delete class raises unauthorized error |
| 1408 # simulate: /rest/data/issue | 1420 # simulate: /rest/data/issue |
| 1409 env = { "REQUEST_METHOD": "DELETE" | 1421 env = { "REQUEST_METHOD": "DELETE" |
| 1410 } | 1422 } |
| 1423 self.server.client.env.update(env) | |
| 1411 headers={"accept": "application/json; version=1", | 1424 headers={"accept": "application/json; version=1", |
| 1412 } | 1425 } |
| 1413 self.headers=headers | 1426 self.headers=headers |
| 1414 self.server.client.request.headers.get=self.get_header | 1427 self.server.client.request.headers.get=self.get_header |
| 1415 results = self.server.dispatch(env["REQUEST_METHOD"], | 1428 results = self.server.dispatch(env["REQUEST_METHOD"], |
| 1439 }' | 1452 }' |
| 1440 env = { "CONTENT_TYPE": "application/jzot", | 1453 env = { "CONTENT_TYPE": "application/jzot", |
| 1441 "CONTENT_LENGTH": len(body), | 1454 "CONTENT_LENGTH": len(body), |
| 1442 "REQUEST_METHOD": "POST" | 1455 "REQUEST_METHOD": "POST" |
| 1443 } | 1456 } |
| 1457 self.server.client.env.update(env) | |
| 1444 | 1458 |
| 1445 headers={"accept": "application/json; version=1", | 1459 headers={"accept": "application/json; version=1", |
| 1446 "content-type": env['CONTENT_TYPE'], | 1460 "content-type": env['CONTENT_TYPE'], |
| 1447 "content-length": env['CONTENT_LENGTH'], | 1461 "content-length": env['CONTENT_LENGTH'], |
| 1448 } | 1462 } |
| 1487 }' | 1501 }' |
| 1488 env = { "CONTENT_TYPE": "application/json", | 1502 env = { "CONTENT_TYPE": "application/json", |
| 1489 "CONTENT_LENGTH": len(body), | 1503 "CONTENT_LENGTH": len(body), |
| 1490 "REQUEST_METHOD": "POST" | 1504 "REQUEST_METHOD": "POST" |
| 1491 } | 1505 } |
| 1492 | 1506 self.server.client.env.update(env) |
| 1493 headers={"accept": "application/zot; version=1; q=0.5", | 1507 headers={"accept": "application/zot; version=1; q=0.5", |
| 1494 "content-type": env['CONTENT_TYPE'], | 1508 "content-type": env['CONTENT_TYPE'], |
| 1495 "content-length": env['CONTENT_LENGTH'], | 1509 "content-length": env['CONTENT_LENGTH'], |
| 1496 } | 1510 } |
| 1497 | 1511 |
| 1516 # is valid | 1530 # is valid |
| 1517 env = { "CONTENT_TYPE": "application/json", | 1531 env = { "CONTENT_TYPE": "application/json", |
| 1518 "CONTENT_LENGTH": len(body), | 1532 "CONTENT_LENGTH": len(body), |
| 1519 "REQUEST_METHOD": "POST" | 1533 "REQUEST_METHOD": "POST" |
| 1520 } | 1534 } |
| 1521 | 1535 self.server.client.env.update(env) |
| 1522 headers={"accept": "application/zot; version=1; q=0.75, " | 1536 headers={"accept": "application/zot; version=1; q=0.75, " |
| 1523 "application/json; version=1; q=0.5", | 1537 "application/json; version=1; q=0.5", |
| 1524 "content-type": env['CONTENT_TYPE'], | 1538 "content-type": env['CONTENT_TYPE'], |
| 1525 "content-length": env['CONTENT_LENGTH'], | 1539 "content-length": env['CONTENT_LENGTH'], |
| 1526 } | 1540 } |
| 1658 # Make sure false value works to suppress @stats | 1672 # Make sure false value works to suppress @stats |
| 1659 form = cgi.FieldStorage() | 1673 form = cgi.FieldStorage() |
| 1660 form.list = [ | 1674 form.list = [ |
| 1661 cgi.MiniFieldStorage('@stats', 'False'), | 1675 cgi.MiniFieldStorage('@stats', 'False'), |
| 1662 ] | 1676 ] |
| 1677 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 1663 results = self.server.dispatch('GET', | 1678 results = self.server.dispatch('GET', |
| 1664 "/rest/data/user/1/realname", | 1679 "/rest/data/user/1/realname", |
| 1665 form) | 1680 form) |
| 1666 self.assertEqual(self.dummy_client.response_code, 200) | 1681 self.assertEqual(self.dummy_client.response_code, 200) |
| 1667 json_dict = json.loads(b2s(results)) | 1682 json_dict = json.loads(b2s(results)) |
| 1715 cgi.MiniFieldStorage('@stats', 'true'), | 1730 cgi.MiniFieldStorage('@stats', 'true'), |
| 1716 ] | 1731 ] |
| 1717 self.headers = headers | 1732 self.headers = headers |
| 1718 self.server.client.request.headers.get = self.get_header | 1733 self.server.client.request.headers.get = self.get_header |
| 1719 self.db.setCurrentUser('admin') # must be admin to change user | 1734 self.db.setCurrentUser('admin') # must be admin to change user |
| 1735 self.server.client.env.update({'REQUEST_METHOD': 'PUT'}) | |
| 1720 results = self.server.dispatch('PUT', | 1736 results = self.server.dispatch('PUT', |
| 1721 "/rest/data/user/1/realname", | 1737 "/rest/data/user/1/realname", |
| 1722 form) | 1738 form) |
| 1723 self.assertEqual(self.dummy_client.response_code, 200) | 1739 self.assertEqual(self.dummy_client.response_code, 200) |
| 1724 json_dict = json.loads(b2s(results)) | 1740 json_dict = json.loads(b2s(results)) |
| 1738 etag = calculate_etag(self.db.user.getnode(self.joeid), | 1754 etag = calculate_etag(self.db.user.getnode(self.joeid), |
| 1739 self.db.config['WEB_SECRET_KEY']) | 1755 self.db.config['WEB_SECRET_KEY']) |
| 1740 body=b'{ "data": "Joe Doe 1" }' | 1756 body=b'{ "data": "Joe Doe 1" }' |
| 1741 env = { "CONTENT_TYPE": "application/json", | 1757 env = { "CONTENT_TYPE": "application/json", |
| 1742 "CONTENT_LENGTH": len(body), | 1758 "CONTENT_LENGTH": len(body), |
| 1743 "REQUEST_METHOD": "PUT" | 1759 "REQUEST_METHOD": "PUT", |
| 1744 } | 1760 "HTTP_ORIGIN": "https://invalid.origin" |
| 1761 } | |
| 1762 self.server.client.env.update(env) | |
| 1763 | |
| 1745 headers={"accept": "application/json; version=1", | 1764 headers={"accept": "application/json; version=1", |
| 1746 "content-type": env['CONTENT_TYPE'], | 1765 "content-type": env['CONTENT_TYPE'], |
| 1747 "content-length": env['CONTENT_LENGTH'], | 1766 "content-length": env['CONTENT_LENGTH'], |
| 1748 "if-match": etag | 1767 "if-match": etag |
| 1749 } | 1768 } |
| 1758 self.server.client.request.headers.get=self.get_header | 1777 self.server.client.request.headers.get=self.get_header |
| 1759 results = self.server.dispatch('PUT', | 1778 results = self.server.dispatch('PUT', |
| 1760 "/rest/data/user/%s/realname"%self.joeid, | 1779 "/rest/data/user/%s/realname"%self.joeid, |
| 1761 form) | 1780 form) |
| 1762 | 1781 |
| 1782 # invalid origin, no credentials allowed. | |
| 1783 self.assertNotIn("Access-Control-Allow-Credentials", | |
| 1784 self.server.client.additional_headers) | |
| 1763 self.assertEqual(self.server.client.response_code, 200) | 1785 self.assertEqual(self.server.client.response_code, 200) |
| 1764 results = self.server.get_element('user', self.joeid, self.empty_form) | 1786 results = self.server.get_element('user', self.joeid, self.empty_form) |
| 1765 self.assertEqual(self.dummy_client.response_code, 200) | 1787 self.assertEqual(self.dummy_client.response_code, 200) |
| 1766 self.assertEqual(results['data']['attributes']['realname'], | 1788 self.assertEqual(results['data']['attributes']['realname'], |
| 1767 'Joe Doe 1') | 1789 'Joe Doe 1') |
| 1839 self.server.client.request.headers.get = self.get_header | 1861 self.server.client.request.headers.get = self.get_header |
| 1840 results = self.server.dispatch('PUT', | 1862 results = self.server.dispatch('PUT', |
| 1841 "/rest/data/user/%s/realname"%self.joeid, | 1863 "/rest/data/user/%s/realname"%self.joeid, |
| 1842 form) | 1864 form) |
| 1843 self.assertEqual(self.dummy_client.response_code, 200) | 1865 self.assertEqual(self.dummy_client.response_code, 200) |
| 1866 self.server.client.env.update({'REQUEST_METHOD': "GET"}) | |
| 1844 results = self.server.dispatch('GET', | 1867 results = self.server.dispatch('GET', |
| 1845 "/rest/data/user/%s/realname"%self.joeid, | 1868 "/rest/data/user/%s/realname"%self.joeid, |
| 1846 self.empty_form) | 1869 self.empty_form) |
| 1847 self.assertEqual(self.dummy_client.response_code, 200) | 1870 self.assertEqual(self.dummy_client.response_code, 200) |
| 1848 json_dict = json.loads(b2s(results)) | 1871 json_dict = json.loads(b2s(results)) |
| 1870 body=s2b('{ "address": "demo2@example.com", "@etag": "\\"%s\\""}'%etagb) | 1893 body=s2b('{ "address": "demo2@example.com", "@etag": "\\"%s\\""}'%etagb) |
| 1871 env = { "CONTENT_TYPE": "application/json", | 1894 env = { "CONTENT_TYPE": "application/json", |
| 1872 "CONTENT_LENGTH": len(body), | 1895 "CONTENT_LENGTH": len(body), |
| 1873 "REQUEST_METHOD": "PATCH" | 1896 "REQUEST_METHOD": "PATCH" |
| 1874 } | 1897 } |
| 1898 self.server.client.env.update(env) | |
| 1875 headers={"accept": "application/json", | 1899 headers={"accept": "application/json", |
| 1876 "content-type": env['CONTENT_TYPE'], | 1900 "content-type": env['CONTENT_TYPE'], |
| 1877 "content-length": len(body) | 1901 "content-length": len(body) |
| 1878 } | 1902 } |
| 1879 self.headers=headers | 1903 self.headers=headers |
| 1923 body=b'{ "title": "foo bar", "priority": "critical" }' | 1947 body=b'{ "title": "foo bar", "priority": "critical" }' |
| 1924 env = { "CONTENT_TYPE": "application/json", | 1948 env = { "CONTENT_TYPE": "application/json", |
| 1925 "CONTENT_LENGTH": len(body), | 1949 "CONTENT_LENGTH": len(body), |
| 1926 "REQUEST_METHOD": "POST" | 1950 "REQUEST_METHOD": "POST" |
| 1927 } | 1951 } |
| 1952 self.server.client.env.update(env) | |
| 1928 headers={"accept": "application/json", | 1953 headers={"accept": "application/json", |
| 1929 "content-type": env['CONTENT_TYPE'], | 1954 "content-type": env['CONTENT_TYPE'], |
| 1930 "content-length": len(body) | 1955 "content-length": len(body) |
| 1931 } | 1956 } |
| 1932 self.headers=headers | 1957 self.headers=headers |
| 1956 body=b'{ "title": "foo bar", "priority": "critical" }' | 1981 body=b'{ "title": "foo bar", "priority": "critical" }' |
| 1957 env = { "CONTENT_TYPE": "application/json", | 1982 env = { "CONTENT_TYPE": "application/json", |
| 1958 "CONTENT_LENGTH": len(body), | 1983 "CONTENT_LENGTH": len(body), |
| 1959 "REQUEST_METHOD": "POST" | 1984 "REQUEST_METHOD": "POST" |
| 1960 } | 1985 } |
| 1986 self.server.client.env.update(env) | |
| 1961 headers={"accept": "application/json", | 1987 headers={"accept": "application/json", |
| 1962 "content-type": env['CONTENT_TYPE'], | 1988 "content-type": env['CONTENT_TYPE'], |
| 1963 "content-length": len(body) | 1989 "content-length": len(body) |
| 1964 } | 1990 } |
| 1965 self.headers=headers | 1991 self.headers=headers |
| 1987 body=b'{ "order": 5 }' | 2013 body=b'{ "order": 5 }' |
| 1988 env = { "CONTENT_TYPE": "application/json", | 2014 env = { "CONTENT_TYPE": "application/json", |
| 1989 "CONTENT_LENGTH": len(body), | 2015 "CONTENT_LENGTH": len(body), |
| 1990 "REQUEST_METHOD": "POST" | 2016 "REQUEST_METHOD": "POST" |
| 1991 } | 2017 } |
| 2018 self.server.client.env.update(env) | |
| 1992 headers={"accept": "application/json; version=1", | 2019 headers={"accept": "application/json; version=1", |
| 1993 "content-type": env['CONTENT_TYPE'], | 2020 "content-type": env['CONTENT_TYPE'], |
| 1994 "content-length": len(body) | 2021 "content-length": len(body) |
| 1995 } | 2022 } |
| 1996 self.headers=headers | 2023 self.headers=headers |
| 2001 self.server.client.request.headers.get=self.get_header | 2028 self.server.client.request.headers.get=self.get_header |
| 2002 self.db.setCurrentUser('admin') # must be admin to create status | 2029 self.db.setCurrentUser('admin') # must be admin to create status |
| 2003 results = self.server.dispatch('POST', | 2030 results = self.server.dispatch('POST', |
| 2004 "/rest/data/status", | 2031 "/rest/data/status", |
| 2005 form) | 2032 form) |
| 2006 | 2033 self.server.client.env.update(env) |
| 2007 self.assertEqual(self.server.client.response_code, 400) | 2034 self.assertEqual(self.server.client.response_code, 400) |
| 2008 json_dict = json.loads(b2s(results)) | 2035 json_dict = json.loads(b2s(results)) |
| 2009 status=json_dict['error']['status'] | 2036 status=json_dict['error']['status'] |
| 2010 msg=json_dict['error']['msg'] | 2037 msg=json_dict['error']['msg'] |
| 2011 self.assertEqual(status, 400) | 2038 self.assertEqual(status, 400) |
| 2019 self.db.config['WEB_SECRET_KEY']) | 2046 self.db.config['WEB_SECRET_KEY']) |
| 2020 etagb = etag.strip ('"') | 2047 etagb = etag.strip ('"') |
| 2021 env = {"CONTENT_TYPE": "application/json", | 2048 env = {"CONTENT_TYPE": "application/json", |
| 2022 "CONTENT_LEN": 0, | 2049 "CONTENT_LEN": 0, |
| 2023 "REQUEST_METHOD": "DELETE" } | 2050 "REQUEST_METHOD": "DELETE" } |
| 2051 self.server.client.env.update(env) | |
| 2024 # use text/plain header and request json output by appending | 2052 # use text/plain header and request json output by appending |
| 2025 # .json to the url. | 2053 # .json to the url. |
| 2026 headers={"accept": "text/plain", | 2054 headers={"accept": "text/plain", |
| 2027 "content-type": env['CONTENT_TYPE'], | 2055 "content-type": env['CONTENT_TYPE'], |
| 2028 "if-match": '"%s"'%etagb, | 2056 "if-match": '"%s"'%etagb, |
| 2042 print(results) | 2070 print(results) |
| 2043 json_dict = json.loads(b2s(results)) | 2071 json_dict = json.loads(b2s(results)) |
| 2044 status=json_dict['data']['status'] | 2072 status=json_dict['data']['status'] |
| 2045 self.assertEqual(status, 'ok') | 2073 self.assertEqual(status, 'ok') |
| 2046 | 2074 |
| 2075 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 2047 results = self.server.dispatch('GET', | 2076 results = self.server.dispatch('GET', |
| 2048 "/rest/data/issuetitle:=asdf.jon", | 2077 "/rest/data/issuetitle:=asdf.jon", |
| 2049 form) | 2078 form) |
| 2050 self.assertEqual(self.server.client.response_code, 406) | 2079 self.assertEqual(self.server.client.response_code, 406) |
| 2051 print(results) | 2080 print(results) |
| 2069 # simulate: /rest/data/issue | 2098 # simulate: /rest/data/issue |
| 2070 form = cgi.FieldStorage() | 2099 form = cgi.FieldStorage() |
| 2071 form.list = [ | 2100 form.list = [ |
| 2072 cgi.MiniFieldStorage('@apiver', 'L'), | 2101 cgi.MiniFieldStorage('@apiver', 'L'), |
| 2073 ] | 2102 ] |
| 2103 | |
| 2104 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 2105 | |
| 2074 headers={"accept": "application/json; notversion=z" } | 2106 headers={"accept": "application/json; notversion=z" } |
| 2075 self.headers=headers | 2107 self.headers=headers |
| 2076 self.server.client.request.headers.get=self.get_header | 2108 self.server.client.request.headers.get=self.get_header |
| 2077 results = self.server.dispatch('GET', | 2109 results = self.server.dispatch('GET', |
| 2078 "/rest/data/issue/1", form) | 2110 "/rest/data/issue/1", form) |
| 2226 self.assertEqual(self.server.client.response_code, 404) | 2258 self.assertEqual(self.server.client.response_code, 404) |
| 2227 | 2259 |
| 2228 del(self.headers) | 2260 del(self.headers) |
| 2229 | 2261 |
| 2230 def testAcceptHeaderParsing(self): | 2262 def testAcceptHeaderParsing(self): |
| 2263 self.server.client.env['REQUEST_METHOD'] = 'GET' | |
| 2264 | |
| 2231 # TEST #1 | 2265 # TEST #1 |
| 2232 # json highest priority | 2266 # json highest priority |
| 2233 self.server.client.request.headers.get=self.get_header | 2267 self.server.client.request.headers.get=self.get_header |
| 2234 headers={"accept": "application/json; version=1," | 2268 headers={"accept": "application/json; version=1," |
| 2235 "application/xml; q=0.5; version=2," | 2269 "application/xml; q=0.5; version=2," |
| 2375 self.headers=headers | 2409 self.headers=headers |
| 2376 form = client.BinaryFieldStorage(body_file, | 2410 form = client.BinaryFieldStorage(body_file, |
| 2377 headers=headers, | 2411 headers=headers, |
| 2378 environ=env) | 2412 environ=env) |
| 2379 self.db.setCurrentUser('admin') # must be admin to create status | 2413 self.db.setCurrentUser('admin') # must be admin to create status |
| 2414 | |
| 2415 self.server.client.env.update({'REQUEST_METHOD': method}) | |
| 2416 | |
| 2380 results = self.server.dispatch(method, | 2417 results = self.server.dispatch(method, |
| 2381 "/rest/data/status", | 2418 "/rest/data/status", |
| 2382 form) | 2419 form) |
| 2383 | 2420 |
| 2384 self.assertEqual(self.server.client.response_code, 400) | 2421 self.assertEqual(self.server.client.response_code, 400) |
| 2405 form = client.BinaryFieldStorage(body_file, | 2442 form = client.BinaryFieldStorage(body_file, |
| 2406 headers=headers, | 2443 headers=headers, |
| 2407 environ=env) | 2444 environ=env) |
| 2408 self.server.client.request.headers.get=self.get_header | 2445 self.server.client.request.headers.get=self.get_header |
| 2409 self.db.setCurrentUser('admin') # must be admin to delete issue | 2446 self.db.setCurrentUser('admin') # must be admin to delete issue |
| 2447 self.server.client.env.update({'REQUEST_METHOD': 'POST'}) | |
| 2410 results = self.server.dispatch('POST', | 2448 results = self.server.dispatch('POST', |
| 2411 "/rest/data/status/1", | 2449 "/rest/data/status/1", |
| 2412 form) | 2450 form) |
| 2413 print(results) | 2451 print(results) |
| 2414 self.assertEqual(self.server.client.response_code, 200) | 2452 self.assertEqual(self.server.client.response_code, 200) |
| 2428 empty_body=b'' | 2466 empty_body=b'' |
| 2429 env = { "CONTENT_TYPE": "application/json", | 2467 env = { "CONTENT_TYPE": "application/json", |
| 2430 "CONTENT_LENGTH": len(empty_body), | 2468 "CONTENT_LENGTH": len(empty_body), |
| 2431 "REQUEST_METHOD": "POST" | 2469 "REQUEST_METHOD": "POST" |
| 2432 } | 2470 } |
| 2471 self.server.client.env.update(env) | |
| 2472 | |
| 2433 headers={"accept": "application/json", | 2473 headers={"accept": "application/json", |
| 2434 "content-type": env['CONTENT_TYPE'], | 2474 "content-type": env['CONTENT_TYPE'], |
| 2435 "content-length": len(empty_body) | 2475 "content-length": len(empty_body) |
| 2436 } | 2476 } |
| 2437 self.headers=headers | 2477 self.headers=headers |
| 2458 body=b'{ "title": "foo bar", "priority": "critical" }' | 2498 body=b'{ "title": "foo bar", "priority": "critical" }' |
| 2459 env = { "CONTENT_TYPE": "application/json", | 2499 env = { "CONTENT_TYPE": "application/json", |
| 2460 "CONTENT_LENGTH": len(body), | 2500 "CONTENT_LENGTH": len(body), |
| 2461 "REQUEST_METHOD": "POST" | 2501 "REQUEST_METHOD": "POST" |
| 2462 } | 2502 } |
| 2503 self.server.client.env.update(env) | |
| 2463 headers={"accept": "application/json", | 2504 headers={"accept": "application/json", |
| 2464 "content-type": env['CONTENT_TYPE'], | 2505 "content-type": env['CONTENT_TYPE'], |
| 2465 "content-length": len(body) | 2506 "content-length": len(body) |
| 2466 } | 2507 } |
| 2467 self.headers=headers | 2508 self.headers=headers |
| 2497 self.assertEqual(results['error']['msg'], | 2538 self.assertEqual(results['error']['msg'], |
| 2498 "POE token \'%s\' not valid."%poe) | 2539 "POE token \'%s\' not valid."%poe) |
| 2499 | 2540 |
| 2500 ## Try using GET on POE url. Should fail with method not | 2541 ## Try using GET on POE url. Should fail with method not |
| 2501 ## allowed (405) | 2542 ## allowed (405) |
| 2543 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) | |
| 2502 self.server.client.request.headers.get=self.get_header | 2544 self.server.client.request.headers.get=self.get_header |
| 2503 results = self.server.dispatch('GET', | 2545 results = self.server.dispatch('GET', |
| 2504 "/rest/data/issue/@poe", | 2546 "/rest/data/issue/@poe", |
| 2505 form) | 2547 form) |
| 2506 self.assertEqual(self.server.client.response_code, 405) | 2548 self.assertEqual(self.server.client.response_code, 405) |
| 2511 body_file=BytesIO(body_poe) # FieldStorage needs a file | 2553 body_file=BytesIO(body_poe) # FieldStorage needs a file |
| 2512 form = client.BinaryFieldStorage(body_file, | 2554 form = client.BinaryFieldStorage(body_file, |
| 2513 headers=headers, | 2555 headers=headers, |
| 2514 environ=env) | 2556 environ=env) |
| 2515 self.server.client.request.headers.get=self.get_header | 2557 self.server.client.request.headers.get=self.get_header |
| 2558 self.server.client.env.update({'REQUEST_METHOD': 'POST'}) | |
| 2516 results = self.server.dispatch('POST', | 2559 results = self.server.dispatch('POST', |
| 2517 "/rest/data/issue/@poe", | 2560 "/rest/data/issue/@poe", |
| 2518 form) | 2561 form) |
| 2519 json_dict = json.loads(b2s(results)) | 2562 json_dict = json.loads(b2s(results)) |
| 2520 url=json_dict['data']['link'] | 2563 url=json_dict['data']['link'] |
| 3423 results = results['data'] | 3466 results = results['data'] |
| 3424 self.assertEqual(self.dummy_client.response_code, 200) | 3467 self.assertEqual(self.dummy_client.response_code, 200) |
| 3425 self.assertEqual(len(results['attributes']['nosy']), 0) | 3468 self.assertEqual(len(results['attributes']['nosy']), 0) |
| 3426 self.assertListEqual(results['attributes']['nosy'], []) | 3469 self.assertListEqual(results['attributes']['nosy'], []) |
| 3427 | 3470 |
| 3471 | |
| 3472 def testRestMatchWildcardOrigin(self): | |
| 3473 # cribbed from testDispatch #1 | |
| 3474 # PUT: joe's 'realname' using json data. | |
| 3475 # simulate: /rest/data/user/<id>/realname | |
| 3476 # use etag in header | |
| 3477 | |
| 3478 # verify that credential header is missing, valid allow origin | |
| 3479 # header and vary includes origin. | |
| 3480 | |
| 3481 local_client = self.server.client | |
| 3482 etag = calculate_etag(self.db.user.getnode(self.joeid), | |
| 3483 self.db.config['WEB_SECRET_KEY']) | |
| 3484 body = b'{ "data": "Joe Doe 1" }' | |
| 3485 env = { "CONTENT_TYPE": "application/json", | |
| 3486 "CONTENT_LENGTH": len(body), | |
| 3487 "REQUEST_METHOD": "PUT", | |
| 3488 "HTTP_ORIGIN": "https://bad.origin" | |
| 3489 } | |
| 3490 local_client.env.update(env) | |
| 3491 | |
| 3492 local_client.db.config["WEB_ALLOWED_API_ORIGINS"] = " * " | |
| 3493 | |
| 3494 headers={"accept": "application/json; version=1", | |
| 3495 "content-type": env['CONTENT_TYPE'], | |
| 3496 "content-length": env['CONTENT_LENGTH'], | |
| 3497 "if-match": etag, | |
| 3498 "origin": env['HTTP_ORIGIN'] | |
| 3499 } | |
| 3500 self.headers=headers | |
| 3501 # we need to generate a FieldStorage the looks like | |
| 3502 # FieldStorage(None, None, 'string') rather than | |
| 3503 # FieldStorage(None, None, []) | |
| 3504 body_file=BytesIO(body) # FieldStorage needs a file | |
| 3505 form = client.BinaryFieldStorage(body_file, | |
| 3506 headers=headers, | |
| 3507 environ=env) | |
| 3508 local_client.request.headers.get=self.get_header | |
| 3509 results = self.server.dispatch('PUT', | |
| 3510 "/rest/data/user/%s/realname"%self.joeid, | |
| 3511 form) | |
| 3512 | |
| 3513 self.assertNotIn("Access-Control-Allow-Credentials", | |
| 3514 local_client.additional_headers) | |
| 3515 | |
| 3516 self.assertIn("Access-Control-Allow-Origin", | |
| 3517 local_client.additional_headers) | |
| 3518 self.assertEqual( | |
| 3519 headers['origin'], | |
| 3520 local_client.additional_headers["Access-Control-Allow-Origin"]) | |
| 3521 | |
| 3522 | |
| 3523 self.assertIn("Vary", local_client.additional_headers) | |
| 3524 self.assertIn("Origin", | |
| 3525 local_client.additional_headers['Vary']) | |
| 3526 | |
| 3527 self.assertEqual(local_client.response_code, 200) | |
| 3528 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 3529 self.assertEqual(self.dummy_client.response_code, 200) | |
| 3530 self.assertEqual(results['data']['attributes']['realname'], | |
| 3531 'Joe Doe 1') | |
| 3532 | |
| 3428 @skip_jwt | 3533 @skip_jwt |
| 3429 def test_expired_jwt(self): | 3534 def test_expired_jwt(self): |
| 3430 # self.dummy_client.main() closes database, so | 3535 # self.dummy_client.main() closes database, so |
| 3431 # we need a new test with setup called for each test | 3536 # we need a new test with setup called for each test |
| 3432 out = [] | 3537 out = [] |
