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_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_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_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_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 upload_file( client: FlaskClient, headers: Dict[str, str], fastq: Path, dataset_uuid: str, stream: bool = True, ) -> Response: # get the data for the upload request filename = fastq.name filesize = fastq.stat().st_size data = { "name": filename, "mimeType": "application/gzip", "size": filesize, "lastModified": int(fastq.stat().st_mtime), } r_post: Response = client.post( f"{API_URI}/dataset/{dataset_uuid}/files/upload", headers=headers, data=data) if r_post.status_code != 201: return r_post chunksize = int(filesize / 2) + 1 range_start = 0 with open(fastq, "rb") as f: while True: read_data = f.read(chunksize) # No more data to send, exit the loop # This case is never reached because in normal conditions # the loop is exited when the APIs respond with a code != 206 if not read_data: # pragma: no cover break if range_start != 0: range_start += 1 range_max = range_start + chunksize if range_max > filesize: range_max = filesize headers[ "Content-Range"] = f"bytes {range_start}-{range_max}/{filesize}" if stream: r: Response = client.put( f"{API_URI}/dataset/{dataset_uuid}/files/upload/{filename}", headers=headers, data=read_data, ) else: # do not read data to test final size!=expected size r = client.put( f"{API_URI}/dataset/{dataset_uuid}/files/upload/{filename}", headers=headers, ) if r.status_code != 206: # the upload is completed or an error occurred break range_start += chunksize return r
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_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_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_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_test_unused_credentials(self, client: FlaskClient, faker: Faker) -> None: assert BaseTests.unused_credentials is not None assert len(BaseTests.unused_credentials) == 3 data = { "username": BaseTests.unused_credentials[0], "password": faker.password(strong=True), } # Credentials are verified before the inactivity check r = client.post(f"{AUTH_URI}/login", json=data) assert r.status_code == 401 resp = self.get_content(r) assert resp == "Invalid access credentials" data = { "username": BaseTests.unused_credentials[0], "password": BaseTests.unused_credentials[1], } # Login is blocked due to inactivity r = client.post(f"{AUTH_URI}/login", json=data) assert r.status_code == 403 resp = self.get_content(r) assert resp == "Sorry, this account is blocked for inactivity" # Also password reset and blocked... how to recover the account !? reset_data = {"reset_email": BaseTests.unused_credentials[0]} r = client.post(f"{AUTH_URI}/reset", json=reset_data) assert r.status_code == 403 resp = self.get_content(r) assert resp == "Sorry, this account is blocked for inactivity" events = self.get_last_events(2) assert events[0].event == Events.refused_login.value assert events[0].payload[ "username"] == BaseTests.unused_credentials[0] assert (events[0].payload["motivation"] == "account blocked due to inactivity") assert events[1].event == Events.refused_login.value assert events[1].payload[ "username"] == BaseTests.unused_credentials[0] assert (events[1].payload["motivation"] == "account blocked due to inactivity") assert events[1].url == "/auth/reset" # Goodbye temporary user self.delete_user(client, BaseTests.unused_credentials[2])
def test_database_exceptions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping dabase exceptions tests") return # This is a special value. The endpoint will try to create a group without # shortname. A BadRequest is expected because the database should refuse the # entry due to the missing property r = client.post(f"{API_URI}/tests/database/400") assert r.status_code == 400 # This is the message of a DatabaseMissingRequiredProperty assert self.get_content(r) == "Missing property shortname required by Group" auth = Connector.get_authentication_instance() default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # the /tests/database endpoint will change the default group fullname # as a side effect to the test the database_transaction decorator default_fullname = default_group.fullname random_name = faker.pystr() # This will create a new group with short/full name == random_name r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 200 default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # As a side effect the fullname of defaut_group is changed... assert default_group.fullname != default_fullname # ... and this is the new name new_fullname = default_group.fullname # This will try to create again a group with short/full name == random_name # but this will fail due to unique keys r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 409 # This is the message of a DatabaseDuplicatedEntry self.get_content(r) == "A Group already exists with 'shortname': '400'" # The default group will not change again because the # database_transaction decorator will undo the change default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None assert default_group.fullname == new_fullname
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_01_login_ban_not_enabled(self, client: FlaskClient) -> None: uuid, data = self.create_user(client) # Login attempts are not registered, let's try to fail the login many times for _ in range(0, 10): self.do_login(client, data["email"], "wrong", status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == data["email"] assert events[0].url == "/auth/login" # and verify that login is still allowed headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == data["email"] assert events[0].url == "/auth/login" # Furthermore the login/unlock endpoint is now enabled r = client.post(f"{AUTH_URI}/login/unlock/token") assert r.status_code == 404 # Goodbye temporary user self.delete_user(client, uuid)
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_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 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_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_inputs(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.getDynamicInputSchema(client, "tests/inputs", {}) data = self.buildData(schema) r = client.post(f"{API_URI}/tests/inputs", data=data) 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_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_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_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 delete_test_env( client: FlaskClient, user_A1_headers: Tuple[Optional[Dict[str, str]], str], user_B1_headers: Tuple[Optional[Dict[str, str]], str], user_B1_uuid: str, user_B2_uuid: str, user_A1_uuid: str, uuid_group_A: str, uuid_group_B: str, study1_uuid: Optional[str] = None, study2_uuid: Optional[str] = None, ) -> None: admin_headers, _ = BaseTests.do_login(client, None, None) # delete all the elements used by the test if study1_uuid: r = client.delete(f"{API_URI}/study/{study1_uuid}", headers=user_B1_headers) assert r.status_code == 204 if study2_uuid: r = client.delete(f"{API_URI}/study/{study2_uuid}", headers=user_A1_headers) assert r.status_code == 204 # first user r = client.delete(f"{API_URI}/admin/users/{user_B1_uuid}", headers=admin_headers) assert r.status_code == 204 # second user r = client.delete(f"{API_URI}/admin/users/{user_B2_uuid}", headers=admin_headers) assert r.status_code == 204 # other user r = client.delete(f"{API_URI}/admin/users/{user_A1_uuid}", headers=admin_headers) assert r.status_code == 204 # group A directory INPUT_ROOT_path = INPUT_ROOT.joinpath(uuid_group_A) shutil.rmtree(INPUT_ROOT_path) # group A r = client.delete(f"{API_URI}/admin/groups/{uuid_group_A}", headers=admin_headers) assert r.status_code == 204 # group B directory INPUT_ROOT_path = INPUT_ROOT.joinpath(uuid_group_B) shutil.rmtree(INPUT_ROOT_path) # group B r = client.delete(f"{API_URI}/admin/groups/{uuid_group_B}", headers=admin_headers) assert r.status_code == 204
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