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 = []

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