def test_autocomplete(self, client: FlaskClient) -> None: # This test verifies that buildData is always able to randomly create # valid inputs for endpoints with inputs defined by marshamallow schemas schema = self.get_dynamic_input_schema(client, "tests/autocomplete", {}) assert schema[0]["key"] == "elements" assert schema[0]["type"] == "string[]" assert "autocomplete_endpoint" in schema[0] assert "autocomplete_id_bind" in schema[0] assert "autocomplete_label_bind" in schema[0] assert "autocomplete_show_id" in schema[0] assert schema[0]["autocomplete_endpoint"] == "/api/tests/autocomplete" assert schema[0]["autocomplete_id_bind"] == "my_id" assert schema[0]["autocomplete_label_bind"] == "my_label" assert schema[0]["autocomplete_show_id"] is True autocomplete_endpoint = f"{SERVER_URI}{schema[0]['autocomplete_endpoint']}" r = client.get(f"{autocomplete_endpoint}/nobody") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 0 r = client.get(f"{autocomplete_endpoint}/oliver") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) > 0 assert schema[0]["autocomplete_id_bind"] in content[0] assert schema[0]["autocomplete_label_bind"] in content[0] r = client.get(f"{autocomplete_endpoint}/s the") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) > 0 assert schema[0]["autocomplete_id_bind"] in content[0] assert schema[0]["autocomplete_label_bind"] in content[0] rand = random.SystemRandom() data = [] for _ in range(0, 3): element = rand.choice(content) data.append(element[schema[0]["autocomplete_id_bind"]]) # put accepts a single id provided by the autocomplete endpoint r = client.put(f"{API_URI}/tests/autocomplete", json={"element": data[0]}) assert r.status_code == 204 # post accepts a list of ids provided by the autocomplete endpoint r = client.post( f"{API_URI}/tests/autocomplete", json={"elements": orjson.dumps(data).decode("UTF8")}, ) assert r.status_code == 204
def test_GET_status(self, client: FlaskClient) -> None: """Test that the flask server is running and reachable""" # Check success alive_message = "Server is alive" log.info("*** VERIFY if API is online") r = client.get(f"{API_URI}/status") assert r.status_code == 200 output = self.get_content(r) assert output == alive_message # Check failure log.info("*** VERIFY if invalid endpoint gives Not Found") r = client.get(API_URI) assert r.status_code == 404 if Env.get_bool("AUTH_ENABLE"): # Check /auth/status with no token or invalid token r = client.get(f"{AUTH_URI}/status") assert r.status_code == 401 r = client.get(f"{AUTH_URI}/status", headers={"Authorization": "Bearer ABC"}) assert r.status_code == 401 else: r = client.get(f"{AUTH_URI}/status") assert r.status_code == 404
def test_04_logout(self, client: FlaskClient) -> None: """Check that you can logout with a valid token""" if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping logout tests") return # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/logout", headers=self.get("auth_header")) assert r.status_code == 204 events = self.get_last_events(2) assert events[0].event == Events.delete.value assert events[0].user == "-" assert events[0].target_type == "Token" assert events[0].url == "/auth/logout" assert events[1].event == Events.logout.value assert events[1].user == BaseAuthentication.default_user assert events[1].url == "/auth/logout" # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/logout") assert r.status_code == 401
def test_vulnerabilities(client: FlaskClient) -> None: strings = ( "xx", "x'x", 'x"x', "x`x", "x#x", "x--x", "x\\*x", "x*x", "x+x", "x;x", "x(x", "x)x", ) for s in strings: r = client.get(f"{API_URI}/tests/vulnerabilities/{s}", query_string={"payload": s}) assert r.status_code == 200 r = client.post(f"{API_URI}/tests/vulnerabilities/{s}", json={"payload": s}) assert r.status_code == 200 # Can't test x//x as url parameter r = client.get(f"{API_URI}/tests/vulnerabilities/x", query_string={"payload": "x//x"}) assert r.status_code == 200 r = client.post(f"{API_URI}/tests/vulnerabilities/x", json={"payload": "x//x"}) assert r.status_code == 200
def test_auth(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/authentication") assert r.status_code == 401 r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 headers, token = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/authentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get( f"{API_URI}/tests/authentication", query_string={"access_token": token} ) assert r.status_code == 401
def test_auth(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return r = client.get(f"{API_URI}/tests/authentication") assert r.status_code == 401 r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 headers, token = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/authentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get(f"{API_URI}/tests/authentication", query_string={"access_token": token}) assert r.status_code == 401
def test_optional_auth(self, client: FlaskClient) -> None: # Optional authentication can accept missing tokens r = client.get(f"{API_URI}/tests/optionalauthentication") assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None headers, token = self.do_login(client, None, None) # Or valid tokens r = client.get(f"{API_URI}/tests/optionalauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user # But not invalid tokens, i.e. if presented the tokens is always validated r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get( f"{API_URI}/tests/optionalauthentication", query_string={"access_token": token}, ) # query token is ignored but the endpoint accepts missing tokens assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None r = client.get( f"{API_URI}/tests/optionalauthentication", query_string={"access_token": "invalid"}, ) # invalid tokens should be rejected, but query token is ignored assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None
def test_cached_authenticated_param_endpoint(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping cache with authentication tests") return headers1, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers1) assert r.status_code == 200 resp1 = self.get_content(r) assert isinstance(resp1, list) # counter is 1 because this is the first request to this endpoint assert resp1[COUNTER] == 1 r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers1) assert r.status_code == 200 resp2 = self.get_content(r) assert isinstance(resp2, list) assert resp2[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp2[COUNTER] == resp1[COUNTER] assert resp2[COUNTER] == 1 headers2, token2 = self.do_login(client, None, None) # Test by using access_token parameter instead of Headers r = client.get(f"{API_URI}/tests/cache/paramauth", query_string={"access_token": token2}) assert r.status_code == 200 resp3 = self.get_content(r) assert isinstance(resp3, list) # This is the same user, uuid is unchanged assert resp3[UUID] == resp1[UUID] # but counter changed, because the response is not replied from the cache assert resp3[COUNTER] != resp1[COUNTER] assert resp3[COUNTER] == 2 r = client.get(f"{API_URI}/tests/cache/paramauth", query_string={"access_token": token2}) assert r.status_code == 200 resp4 = self.get_content(r) assert isinstance(resp4, list) assert resp4[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp4[COUNTER] == resp3[COUNTER] assert resp4[COUNTER] == 2 # Cache is stored starting from the access_token parameter, # but the token is the same also if provided as header r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers2) assert r.status_code == 200 resp5 = self.get_content(r) assert isinstance(resp5, list) assert resp5[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp5[COUNTER] == resp3[COUNTER] assert resp5[COUNTER] == 2
def test_GET_specs(self, client: FlaskClient) -> None: # Alias for /api/specs r = client.get(f"{API_URI}/swagger") assert r.status_code == 200 r = client.get(f"{API_URI}/specs") assert r.status_code == 200 content = self.get_content(r) assert "host" in content assert "info" in content assert "swagger" in content assert "schemes" in content assert "paths" in content assert "definitions" in content assert "/api/admin/users" not in content["paths"] # Not available in new spec... to be introduced? assert "basePath" not in content assert "consumes" not in content assert "produces" not in content # assert "application/json" in content["consumes"] # assert "application/json" in content["produces"] assert "tags" in content # This is no longer in root definition # Now it is set for each endpoint, when required assert "security" not in content # assert "Bearer" in content["security"][0] assert "securityDefinitions" in content assert "Bearer" in content["securityDefinitions"] headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/swagger", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert "host" in content assert "info" in content assert "swagger" in content assert "schemes" in content assert "paths" in content assert "definitions" in content assert "/auth/logout" in content["paths"] # Not available in new spec... to be introduced? assert "basePath" not in content assert "consumes" not in content assert "produces" not in content # assert "application/json" in content["consumes"] # assert "application/json" in content["produces"] assert "tags" in content # This is no longer in root definition # Now it is set for each endpoint, when required assert "security" not in content # assert "Bearer" in content["security"][0] assert "securityDefinitions" in content assert "Bearer" in content["securityDefinitions"]
def test_authentication_with_auth_callback(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None VALID = f"/tests/preloadcallback/{user.uuid}" INVALID = "/tests/preloadcallback/12345678-90ab-cdef-1234-567890abcdef" admin_headers, _ = self.do_login(client, None, None) # Verify both endpoint ... r = client.get(f"{API_URI}{VALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == user.email r = client.get(f"{API_URI}{INVALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 401 # and get_schema! r = client.get( f"{API_URI}{VALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 1 assert content[0]["key"] == "test" assert content[0]["type"] == "boolean" r = client.get( f"{API_URI}{INVALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 401
def test_optional_access_token_parameter(self, client: FlaskClient) -> None: # Optional authentication can accept missing tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication") assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None headers, token = self.do_login(client, None, None) # Or valid tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user # But not invalid tokens, i.e. if presented the tokens is always validated r = client.get( f"{API_URI}/tests/optionalqueryauthentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": token}, ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": "invalid"}, ) # invalid tokens should be rejected, but query token is ignored assert r.status_code == 401
def test_endpoint(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/neo4j/1") assert r.status_code == 200 r = client.get(f"{API_URI}/tests/neo4j/2") assert r.status_code == 400 r = client.get(f"{API_URI}/tests/neo4j/3") assert r.status_code == 200 data = self.get_content(r) data["created"] = dateutil.parser.parse(data["created"]) data["modified1"] = dateutil.parser.parse(data["modified1"]) data["modified2"] = dateutil.parser.parse(data["modified2"]) assert data["created"] < data["modified1"] assert data["modified1"] < data["modified2"]
def test_optional_access_token_parameter(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return # Optional authentication can accept missing tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication") assert r.status_code == 204 headers, token = self.do_login(client, None, None) # Or valid tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user # But not invalid tokens, i.e. if presented the tokens is always validated r = client.get( f"{API_URI}/tests/optionalqueryauthentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": token}, ) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": "invalid"}, ) # invalid tokens should be rejected, but query token is ignored assert r.status_code == 401
def test_caching_general_clearing(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) else: headers = None # get method is cached for 200 seconds # First response is not cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter1 = self.get_content(r) # Second response is cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter1 # Empty all the cache Cache.clear() # Third response is no longer cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter2 = self.get_content(r) assert counter2 != counter1 # Response is still cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter2 # Empty the endpoint cache client.delete(f"{API_URI}/tests/cache/long") # Second response is no longer cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter3 = self.get_content(r) assert counter3 != counter2 # Response is still cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter3 # Endpoint is unauthenticated, headers are ignored when building the cache key r = client.get(f"{API_URI}/tests/cache/long", headers=headers) assert r.status_code == 200 assert self.get_content(r) == counter3 # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/cache/long", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == counter3
def test_depends_on(self, client: FlaskClient) -> None: if Connector.check_availability("neo4j"): r = client.get(f"{API_URI}/tests/depends_on/neo4j") assert r.status_code == 200 r = client.get(f"{API_URI}/tests/depends_on_not/neo4j") assert r.status_code == 404 else: r = client.get(f"{API_URI}/tests/depends_on/neo4j") assert r.status_code == 404 r = client.get(f"{API_URI}/tests/depends_on_not/neo4j") assert r.status_code == 200
def test_05_login_failures(self, client: FlaskClient) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE") and Env.get_bool("AUTH_ENABLE"): # Create a new user on the fly to test the cached endpoint _, data = self.create_user(client) headers, _ = self.do_login( client, data["email"], data["password"], test_failures=True ) r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204
def test_no_auth(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/noauth") assert r.status_code == 200 assert self.get_content(r) == "OK" if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) # Tokens are ignored r = client.get(f"{API_URI}/tests/noauth", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "OK" # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/noauth", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == "OK"
def test_admin_stats(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping admin/logins tests") return r = client.get(f"{API_URI}/admin/logins") assert r.status_code == 401 random_username = faker.ascii_email() self.do_login(client, random_username, faker.pystr(), status_code=401) headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/logins", headers=headers) assert r.status_code == 200 logins = self.get_content(r) assert isinstance(logins, list) assert len(logins) > 0 assert "username" in logins[0] assert "date" in logins[0] assert "IP" in logins[0] assert "location" in logins[0] assert "failed" in logins[0] assert "flushed" in logins[0] for login in logins: if login["username"] == BaseAuthentication.default_user: break else: # pragma: no cover pytest.fail("Default user not found in logins table") for login in logins: if login["username"] == random_username: assert login["failed"] is True assert login["flushed"] is False break else: # pragma: no cover pytest.fail("Random user not found in logins table")
def test_access_token_parameter(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/queryauthentication") assert r.status_code == 401 r = client.get( f"{API_URI}/tests/queryauthentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 headers, token = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/queryauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/queryauthentication", query_string={"access_token": token} ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/queryauthentication", query_string={"access_token": "invalid"}, ) assert r.status_code == 401
def test_cached_semiauthenticated_endpoint(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping cache with authentication tests") return r = client.get(f"{API_URI}/tests/cache/optionalauth") assert r.status_code == 200 nonauthenticated1 = self.get_content(r) assert isinstance(nonauthenticated1, list) assert nonauthenticated1[UUID] == "N/A" # counter is 1 because this is the first request to this endpoint assert nonauthenticated1[COUNTER] == 1 r = client.get(f"{API_URI}/tests/cache/optionalauth") assert r.status_code == 200 nonauthenticated2 = self.get_content(r) assert isinstance(nonauthenticated2, list) assert nonauthenticated2[UUID] == "N/A" # Same counter as above, because the response is replied from the cache assert nonauthenticated2[COUNTER] == 1 headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated1 = self.get_content(r) assert isinstance(authenticated1, list) assert authenticated1[UUID] != "N/A" # The counter changed, because the response is not replied from the cache assert authenticated1[COUNTER] == 2 # Token cached => cache should be used r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated2 = self.get_content(r) assert isinstance(authenticated2, list) assert authenticated2[UUID] == authenticated1[UUID] # Same counter as above, because the response is replied from the cache assert authenticated2[COUNTER] == 2 # New token => no cache headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated2 = self.get_content(r) assert isinstance(authenticated2, list) assert authenticated2[UUID] == authenticated1[UUID] # Counter changed assert authenticated2[COUNTER] == 3 r = client.get( f"{API_URI}/tests/cache/optionalauth", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401
def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE") and Env.get_bool("AUTH_ENABLE"): if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10: headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 200 time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD")) r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 401 # After the failure the token is still valid if used from the correct IP r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Another option to provide IP is through the header passed by nginx # This only works if PROXIED_CONNECTION is on # (disabled by default, for security purpose) if Env.get_bool("PROXIED_CONNECTION"): headers["X-Forwarded-For"] = faker.ipv4() # type: ignore r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401
def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE"): if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10: headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 200 time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD")) r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) log.error( "DEBUG CODE: this 401 should be due to invalid IP address") assert r.status_code == 401 # After the failure the token is still valid if used from the corret IP r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Another option to provide IP is through the header passed by nginx headers["X-Forwarded-For"] = faker.ipv4() # type: ignore r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401
def test_caching_autocleaning(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) else: headers = None # Syncronize this test to start at the beginning of the next second and # prevent the test to overlap a change of second # Since the caching is rounded to the second, few milliseconds cann make the # difference, for example: # A first request at 00:00:00.997 is cached # A second request at 00:00:01.002 is no longer cached, even if only 5 millisec # elapsed because the second changed # Added 0.01 just to avoid to exactly start at the beginning of the second t = 1.01 - datetime.now().microsecond / 1000000.0 log.critical("Sleeping {} sec", t) time.sleep(t) # the GET method is cached for 1 second # First response is not cached r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 counter1 = self.get_content(r) # Second response is cached r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 assert self.get_content(r) == counter1 # Third response is no longer cached time.sleep(1) r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 counter2 = self.get_content(r) assert counter2 != counter1 # Fourth response is cached again r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 assert self.get_content(r) == counter2 # Endpoint is unauthenticated, headers are ignored when building the cache key r = client.get(f"{API_URI}/tests/cache/short", headers=headers) assert r.status_code == 200 assert self.get_content(r) == counter2 # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/cache/short", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == counter2
def test_parameter_injection(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/inject/myparam", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 3 # User is injected by the authentication decorator assert response[0] == BaseAuthentication.default_user # myparam is injected as url parameter assert response[1] == "myparam" # default_value is injected only because it has a... default value assert response[2] == "default_value"
def check_endpoint( self, client: FlaskClient, method: str, endpoint: str, headers: Optional[Dict[str, str]], expected_authorized: bool, paths: List[str], ) -> List[str]: assert method in ( "GET", "POST", "PUT", "PATCH", "DELETE", ) path = self.get_path(method, endpoint) assert path in paths # SERVER_URI because api and auth are already included in endpoint full_endpoint = f"{SERVER_URI}/{endpoint}" if method == "GET": r = client.get(full_endpoint, headers=headers) elif method == "POST": r = client.post(full_endpoint, headers=headers) elif method == "PUT": r = client.put(full_endpoint, headers=headers) elif method == "PATCH": r = client.patch(full_endpoint, headers=headers) elif method == "DELETE": r = client.delete(full_endpoint, headers=headers) else: # pragma: no cover pytest.fail("Unknown method") if expected_authorized: assert r.status_code != 401 else: assert r.status_code != 400 paths.remove(path) return paths
def test_authentication_with_multiple_roles(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/manyrolesauthentication") assert r.status_code == 401 r = client.get(f"{API_URI}/tests/unknownroleauthentication") assert r.status_code == 401 admin_headers, admin_token = self.do_login(client, None, None) r = client.get( f"{API_URI}/tests/manyrolesauthentication", headers=admin_headers ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == admin_token assert content["user"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/unknownroleauthentication", headers=admin_headers ) assert r.status_code == 401 if Env.get_bool("MAIN_LOGIN_ENABLE"): uuid, data = self.create_user(client, roles=[Role.USER]) user_header, user_token = self.do_login( client, data.get("email"), data.get("password") ) r = client.get( f"{API_URI}/tests/manyrolesauthentication", headers=user_header ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == user_token assert content["user"] == data.get("email") r = client.get( f"{API_URI}/tests/unknownroleauthentication", headers=user_header ) assert r.status_code == 401 self.delete_user(client, uuid)
def test_authentication_with_multiple_roles(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return r = client.get(f"{API_URI}/tests/manyrolesauthentication") assert r.status_code == 401 r = client.get(f"{API_URI}/tests/unknownroleauthentication") assert r.status_code == 401 admin_headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/manyrolesauthentication", headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user r = client.get(f"{API_URI}/tests/unknownroleauthentication", headers=admin_headers) assert r.status_code == 401 if Env.get_bool("MAIN_LOGIN_ENABLE"): uuid, data = self.create_user(client, roles=[Role.USER]) user_header, _ = self.do_login(client, data.get("email"), data.get("password")) r = client.get(f"{API_URI}/tests/manyrolesauthentication", headers=user_header) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == data.get("email") r = client.get(f"{API_URI}/tests/unknownroleauthentication", headers=user_header) assert r.status_code == 401 self.delete_user(client, uuid)
def test_download(self, client: FlaskClient) -> None: self.fname = self.get("fname") self.fcontent = self.get("fcontent") r = client.get(f"{API_URI}/tests/download/doesnotexist") assert r.status_code == 400 # no filename provided r = client.get(f"{API_URI}/tests/download") assert r.status_code == 400 r = client.get(f"{API_URI}/tests/download/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == self.fcontent new_content = "new content" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(new_content)), self.fname), "force": True, }, ) assert r.status_code == 200 r = client.get(f"{API_URI}/tests/download/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content != self.fcontent assert content == new_content r = client.get(f"{API_URI}/tests/download/{self.fname}", query_string={"stream": True}) assert r.status_code == 200 content = r.data.decode("utf-8") assert content == new_content r = client.get(f"{API_URI}/tests/download/doesnotexist", query_string={"stream": True}) assert r.status_code == 400
def test_chunked(self, client: FlaskClient, faker: Faker) -> None: self.fname = self.get("fname") self.fcontent = self.get("fcontent") r = client.post(f"{API_URI}/tests/upload", data={"force": True}) assert r.status_code == 400 data = { "force": True, "name": "fixed.filename", "size": "999", "mimeType": "application/zip", "lastModified": 1590302749209, } r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 201 assert self.get_content(r) == "" with io.StringIO(faker.text()) as f: r = client.put(f"{API_URI}/tests/upload/chunked", data=f) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" with io.StringIO(faker.text()) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": "!"}, ) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" up_data = faker.pystr(min_chars=24, max_chars=48) STR_LEN = len(up_data) with io.StringIO(up_data[0:5]) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes 0-5/{STR_LEN}"}, ) assert r.status_code == 206 assert self.get_content(r) == "partial" with io.StringIO(up_data[5:]) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes 5-{STR_LEN}/{STR_LEN}"}, ) assert r.status_code == 200 c = self.get_content(r) assert c.get("filename") is not None uploaded_filename = c.get("filename") meta = c.get("meta") assert meta is not None assert meta.get("charset") == "us-ascii" assert meta.get("type") == "text/plain" r = client.get(f"{API_URI}/tests/download/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get(f"{API_URI}/tests/download/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get(f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": ""}) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"0-{STR_LEN - 1}"}, ) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": "bytes=0-9999999999999999"}, ) from werkzeug import __version__ as werkzeug_version # Back-compatibility check for B2STAGE if werkzeug_version == "0.16.1": # pragma: no cover assert r.status_code == 200 else: assert r.status_code == 206 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": "bytes=0-4"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[0:5] r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"bytes=5-{STR_LEN - 1}"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[5:] r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"bytes=0-{STR_LEN - 1}"}, ) # Back-compatibility check for B2STAGE if werkzeug_version == "0.16.1": # pragma: no cover assert r.status_code == 200 else: assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data # Send a new string as content file. Will be appended as prefix up_data2 = faker.pystr(min_chars=24, max_chars=48) STR_LEN = len(up_data2) with io.StringIO(up_data2) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes */{STR_LEN}"}, ) assert r.status_code == 200 # c = self.get_content(r) # assert c.get('filename') is not None # uploaded_filename = c.get('filename') # meta = c.get('meta') # assert meta is not None # assert meta.get('charset') == 'us-ascii' # assert meta.get('type') == 'text/plain' # r = client.get(f'{API_URI}/tests/download/{uploaded_filename}') # assert r.status_code == 200 # content = r.data.decode('utf-8') # # Uhmmm... should not be up_data2 + up_data ?? # assert content == up_data + up_data2 data["force"] = False r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 400 err = f"File '{uploaded_filename}' already exists" assert self.get_content(r) == err data["force"] = True r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 201 assert self.get_content(r) == ""
def test_02_GET_profile(self, client: FlaskClient, faker: Faker) -> None: """ Check if you can use your token for protected endpoints """ # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/profile", headers=self.get("auth_header")) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/profile") assert r.status_code == 401 # Token created for a fake user token = self.get_crafted_token("f") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("x") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_algorithm=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_secret=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # token created for the correct user, but from outside the system!! token = self.get_crafted_token("f", user_id=uuid) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Immature token token = self.get_crafted_token("f", user_id=uuid, immature=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Expired token token = self.get_crafted_token("f", user_id=uuid, expired=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Sending malformed tokens headers = {"Authorization": "Bearer"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"Bearer '{faker.pystr()}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 # Bearer realm is expected to be case sensitive token = self.get("auth_token") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 headers = {"Authorization": f"bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"BEARER {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 token = self.get("auth_token") headers = {"Authorization": f"Bear {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 USER = BaseAuthentication.default_user PWD = BaseAuthentication.default_password # Testing Basic Authentication (not allowed) credentials = f"{USER}:{PWD}" encoded_credentials = base64.b64encode( str.encode(credentials)).decode("utf-8") headers = {"Authorization": f"Basic {encoded_credentials}"} r = client.post(f"{AUTH_URI}/login", headers=headers) # Response is: # { # 'password': ['Missing data for required field.'], # 'username': ['Missing data for required field.'] # } assert r.status_code == 400 r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401