diff --git a/swh/web/api/views/graph.py b/swh/web/api/views/graph.py --- a/swh/web/api/views/graph.py +++ b/swh/web/api/views/graph.py @@ -9,6 +9,7 @@ import requests +from django.http import QueryDict from django.http.response import StreamingHttpResponse from rest_framework.decorators import renderer_classes from rest_framework.renderers import JSONRenderer @@ -135,10 +136,25 @@ return Response( "You do not have permission to perform this action.", status=403 ) - graph_query_url = get_config()["graph"]["server_url"] + + graph_config = get_config()["graph"] + graph_query_url = graph_config["server_url"] graph_query_url += graph_query - if request.GET: - graph_query_url += "?" + request.GET.urlencode(safe="/;:") + query = request.GET.urlencode(safe="/;:") if request.GET else "" + query_dict = QueryDict(query, mutable=True) + + # clamp max_edges query parameter according to authentication + if request.user.is_staff: + max_edges = graph_config["max_edges"]["staff"] + elif request.user.is_authenticated: + max_edges = graph_config["max_edges"]["user"] + else: + max_edges = graph_config["max_edges"]["anonymous"] + query_dict["max_edges"] = min( + max_edges, int(query_dict.get("max_edges", max_edges + 1)) + ) + + graph_query_url += "?" + query_dict.urlencode(safe="/;:") response = requests.get(graph_query_url, stream=True) # graph stats and counter endpoint responses are not streamed if response.headers.get("Transfer-Encoding") != "chunked": diff --git a/swh/web/config.py b/swh/web/config.py --- a/swh/web/config.py +++ b/swh/web/config.py @@ -110,7 +110,10 @@ "keycloak": ("dict", {"server_url": "", "realm_name": ""}), "graph": ( "dict", - {"server_url": "http://graph.internal.softwareheritage.org:5009/graph/"}, + { + "server_url": "http://graph.internal.softwareheritage.org:5009/graph/", + "max_edges": {"staff": 0, "user": 100000, "anonymous": 1000}, + }, ), "status": ( "dict", diff --git a/swh/web/tests/api/views/test_graph.py b/swh/web/tests/api/views/test_graph.py --- a/swh/web/tests/api/views/test_graph.py +++ b/swh/web/tests/api/views/test_graph.py @@ -34,8 +34,10 @@ check_http_get_response(api_client, url, status_code=401) -def _authenticate_graph_user(api_client, keycloak_oidc): +def _authenticate_graph_user(api_client, keycloak_oidc, is_staff=False): keycloak_oidc.client_permissions = [API_GRAPH_PERM] + if is_staff: + keycloak_oidc.user_groups = ["/staff"] oidc_profile = keycloak_oidc.login() api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {oidc_profile['refresh_token']}") @@ -268,3 +270,82 @@ assert resp.content_type == "application/json" assert resp.data["exception"] == "NotAcceptable" assert resp.data["reason"] == "Could not satisfy the request Accept header." + + +def test_graph_endpoint_max_edges_settings(api_client, keycloak_oidc, requests_mock): + graph_config = get_config()["graph"] + graph_query = "stats" + url = reverse("api-1-graph", url_args={"graph_query": graph_query}) + requests_mock.get( + get_config()["graph"]["server_url"] + graph_query, + json={}, + headers={"Content-Type": "application/json"}, + ) + + # currently unauthenticated user can only use the graph endpoint from + # Software Heritage VPN + check_http_get_response( + api_client, url, status_code=200, server_name=SWH_WEB_INTERNAL_SERVER_NAME + ) + assert ( + f"max_edges={graph_config['max_edges']['anonymous']}" + in requests_mock.request_history[0].url + ) + + # standard user + _authenticate_graph_user(api_client, keycloak_oidc) + check_http_get_response( + api_client, url, status_code=200, + ) + assert ( + f"max_edges={graph_config['max_edges']['user']}" + in requests_mock.request_history[1].url + ) + + # staff user + _authenticate_graph_user(api_client, keycloak_oidc, is_staff=True) + check_http_get_response( + api_client, url, status_code=200, + ) + assert ( + f"max_edges={graph_config['max_edges']['staff']}" + in requests_mock.request_history[2].url + ) + + +def test_graph_endpoint_max_edges_query_parameter_value( + api_client, keycloak_oidc, requests_mock +): + graph_config = get_config()["graph"] + graph_query = "stats" + + requests_mock.get( + get_config()["graph"]["server_url"] + graph_query, + json={}, + headers={"Content-Type": "application/json"}, + ) + _authenticate_graph_user(api_client, keycloak_oidc) + + max_edges_max_value = graph_config["max_edges"]["user"] + + max_edges = max_edges_max_value // 2 + url = reverse( + "api-1-graph", + url_args={"graph_query": graph_query}, + query_params={"max_edges": max_edges}, + ) + check_http_get_response( + api_client, url, status_code=200, + ) + assert f"max_edges={max_edges}" in requests_mock.request_history[0].url + + max_edges = max_edges_max_value * 2 + url = reverse( + "api-1-graph", + url_args={"graph_query": graph_query}, + query_params={"max_edges": max_edges}, + ) + check_http_get_response( + api_client, url, status_code=200, + ) + assert f"max_edges={max_edges_max_value}" in requests_mock.request_history[1].url