From dbe3b7b5c59f8c04809433a2b24f6350b869357d Mon Sep 17 00:00:00 2001 From: PaulineMauryL Date: Mon, 8 Jul 2024 15:58:46 +0200 Subject: [PATCH 1/4] verify user and dataset are alloew for query --- .../admin_database/admin_database.py | 20 ++++++-- .../admin_database/mongodb_database.py | 6 +++ .../admin_database/yaml_database.py | 7 ++- server/lomas_server/routes_admin.py | 26 +++++++++- server/lomas_server/routes_dp.py | 42 ++++++++++++++- server/lomas_server/tests/test_api.py | 51 ++++++++++++++++--- server/requirements.txt | 4 +- 7 files changed, 137 insertions(+), 19 deletions(-) diff --git a/server/lomas_server/admin_database/admin_database.py b/server/lomas_server/admin_database/admin_database.py index ae0c3198..1284f093 100644 --- a/server/lomas_server/admin_database/admin_database.py +++ b/server/lomas_server/admin_database/admin_database.py @@ -1,7 +1,7 @@ import argparse -import functools import time from abc import ABC, abstractmethod +from functools import wraps from typing import Callable, Dict, List from constants import DPLibraries @@ -29,7 +29,7 @@ def user_must_exist(func: Callable) -> Callable: # type: ignore before calling func. """ - @functools.wraps(func) + @wraps(func) def wrapper_decorator( self, *args: argparse.Namespace, **kwargs: Dict[str, str] ) -> None: @@ -61,14 +61,14 @@ def dataset_must_exist(func: Callable) -> Callable: # type: ignore before calling the wrapped function. """ - @functools.wraps(func) + @wraps(func) def wrapper_decorator( self, *args: argparse.Namespace, **kwargs: Dict[str, str] ) -> None: dataset_name = args[0] if not self.does_dataset_exist(dataset_name): raise InvalidQueryException( - f"Dataset {dataset_name} does not exists. " + f"Dataset {dataset_name} does not exist. " + "Please, verify the client object initialisation.", ) return func(self, *args, **kwargs) @@ -97,12 +97,22 @@ def user_must_have_access_to_dataset( to the dataset before calling the wrapped function. """ - @functools.wraps(func) + @wraps(func) def wrapper_decorator( self, *args: argparse.Namespace, **kwargs: Dict[str, str] ) -> None: user_name = args[0] dataset_name = args[1] + if not self.does_user_exist(user_name): + raise UnauthorizedAccessException( + f"User {user_name} does not exist. " + + "Please, verify the client object initialisation.", + ) + if not self.does_dataset_exist(dataset_name): + raise InvalidQueryException( + f"Dataset {dataset_name} does not exist. " + + "Please, verify the client object initialisation.", + ) if not self.has_user_access_to_dataset(user_name, dataset_name): raise UnauthorizedAccessException( f"{user_name} does not have access to {dataset_name}.", diff --git a/server/lomas_server/admin_database/mongodb_database.py b/server/lomas_server/admin_database/mongodb_database.py index 34634867..0f8509f3 100644 --- a/server/lomas_server/admin_database/mongodb_database.py +++ b/server/lomas_server/admin_database/mongodb_database.py @@ -11,6 +11,7 @@ user_must_exist, user_must_have_access_to_dataset, ) +from utils.error_handler import InvalidQueryException class AdminMongoDatabase(AdminDatabase): @@ -134,6 +135,11 @@ def has_user_access_to_dataset( Returns: bool: True if the user has access, False otherwise. """ + if not self.does_dataset_exist(dataset_name): + raise InvalidQueryException( + f"Dataset {dataset_name} does not exist. " + + "Please, verify the client object initialisation.", + ) doc_count = self.db.users.count_documents( { "user_name": f"{user_name}", diff --git a/server/lomas_server/admin_database/yaml_database.py b/server/lomas_server/admin_database/yaml_database.py index a2cf84da..94cc12e6 100644 --- a/server/lomas_server/admin_database/yaml_database.py +++ b/server/lomas_server/admin_database/yaml_database.py @@ -9,7 +9,7 @@ user_must_exist, user_must_have_access_to_dataset, ) -from utils.error_handler import InternalServerException +from utils.error_handler import InternalServerException, InvalidQueryException class AdminYamlDatabase(AdminDatabase): @@ -142,6 +142,11 @@ def has_user_access_to_dataset( Returns: bool: True if the user has access, False otherwise. """ + if not self.does_dataset_exist(dataset_name): + raise InvalidQueryException( + f"Dataset {dataset_name} does not exist. " + + "Please, verify the client object initialisation.", + ) for user in self.database["users"]: if user["user_name"] == user_name: for dataset in user["datasets_list"]: diff --git a/server/lomas_server/routes_admin.py b/server/lomas_server/routes_admin.py index aa6a859c..7c0c7b52 100644 --- a/server/lomas_server/routes_admin.py +++ b/server/lomas_server/routes_admin.py @@ -2,7 +2,11 @@ from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse from dp_queries.dummy_dataset import make_dummy_dataset -from utils.error_handler import KNOWN_EXCEPTIONS, InternalServerException +from utils.error_handler import ( + KNOWN_EXCEPTIONS, + InternalServerException, + UnauthorizedAccessException, +) from utils.example_inputs import ( example_get_admin_db_data, example_get_dummy_dataset, @@ -76,6 +80,7 @@ async def get_memory_usage() -> JSONResponse: def get_dataset_metadata( _request: Request, query_json: GetDbData = Body(example_get_admin_db_data), + user_name: str = Header(None), ) -> JSONResponse: """ Retrieves metadata for a given dataset. @@ -97,9 +102,17 @@ def get_dataset_metadata( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + try: ds_metadata = app.state.admin_database.get_dataset_metadata( - query_json.dataset_name + dataset_name ) except KNOWN_EXCEPTIONS as e: @@ -119,6 +132,7 @@ def get_dataset_metadata( def get_dummy_dataset( _request: Request, query_json: GetDummyDataset = Body(example_get_dummy_dataset), + user_name: str = Header(None), ) -> StreamingResponse: """ Generates and returns a dummy dataset. @@ -143,6 +157,14 @@ def get_dummy_dataset( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + try: ds_metadata = app.state.admin_database.get_dataset_metadata( query_json.dataset_name diff --git a/server/lomas_server/routes_dp.py b/server/lomas_server/routes_dp.py index 17a43b0e..e6241f99 100644 --- a/server/lomas_server/routes_dp.py +++ b/server/lomas_server/routes_dp.py @@ -4,7 +4,11 @@ from constants import DPLibraries from dp_queries.dp_libraries.utils import querier_factory from dp_queries.dummy_dataset import get_dummy_dataset_for_query -from utils.error_handler import KNOWN_EXCEPTIONS, InternalServerException +from utils.error_handler import ( + KNOWN_EXCEPTIONS, + InternalServerException, + UnauthorizedAccessException, +) from utils.example_inputs import ( example_dummy_opendp, example_dummy_smartnoise_sql, @@ -99,6 +103,7 @@ def smartnoise_sql_handler( def dummy_smartnoise_sql_handler( _request: Request, query_json: DummySNSQLInp = Body(example_dummy_smartnoise_sql), + user_name: str = Header(None), ) -> JSONResponse: """ Handles queries on dummy datasets for the SmartNoiseSQL library. @@ -140,6 +145,14 @@ def dummy_smartnoise_sql_handler( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + ds_private_dataset = get_dummy_dataset_for_query( app.state.admin_database, query_json ) @@ -166,6 +179,7 @@ def dummy_smartnoise_sql_handler( def estimate_smartnoise_cost( _request: Request, query_json: SNSQLInpCost = Body(example_smartnoise_sql_cost), + user_name: str = Header(None), ) -> JSONResponse: """ Estimates the privacy loss budget cost of a SmartNoiseSQL query. @@ -198,6 +212,14 @@ def estimate_smartnoise_cost( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + try: response = app.state.query_handler.estimate_cost( DPLibraries.SMARTNOISE_SQL, @@ -279,6 +301,7 @@ def opendp_query_handler( def dummy_opendp_query_handler( _request: Request, query_json: DummyOpenDPInp = Body(example_dummy_opendp), + user_name: str = Header(None), ) -> JSONResponse: """ Handles queries on dummy datasets for the OpenDP library. @@ -317,6 +340,14 @@ def dummy_opendp_query_handler( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + ds_private_dataset = get_dummy_dataset_for_query( app.state.admin_database, query_json ) @@ -345,6 +376,7 @@ def dummy_opendp_query_handler( def estimate_opendp_cost( _request: Request, query_json: OpenDPInp = Body(example_opendp), + user_name: str = Header(None), ) -> JSONResponse: """ Estimates the privacy loss budget cost of an OpenDP query. @@ -371,6 +403,14 @@ def estimate_opendp_cost( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + try: response = app.state.query_handler.estimate_cost( DPLibraries.OPENDP, diff --git a/server/lomas_server/tests/test_api.py b/server/lomas_server/tests/test_api.py index 7ec72f7f..7ff37035 100644 --- a/server/lomas_server/tests/test_api.py +++ b/server/lomas_server/tests/test_api.py @@ -208,7 +208,7 @@ def test_get_dataset_metadata(self) -> None: assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == { "InvalidQueryException": f"Dataset {fake_dataset} does not " - + "exists. Please, verify the client object initialisation." + + "exist. Please, verify the client object initialisation." } def test_get_dummy_dataset(self) -> None: @@ -216,7 +216,9 @@ def test_get_dummy_dataset(self) -> None: with TestClient(app) as client: # Expect to work response = client.post( - "/get_dummy_dataset", json=example_get_dummy_dataset + "/get_dummy_dataset", + json=example_get_dummy_dataset, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK @@ -243,7 +245,7 @@ def test_get_dummy_dataset(self) -> None: assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == { "InvalidQueryException": f"Dataset {fake_dataset} does not " - + "exists. Please, verify the client object initialisation." + + "exist. Please, verify the client object initialisation." } # Expect to fail: missing argument dummy_nb_rows @@ -256,6 +258,21 @@ def test_get_dummy_dataset(self) -> None: ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + # Expect to fail: user does not exist + fake_user = "fake_user" + new_headers = self.headers + new_headers["user-name"] = fake_user + response = client.post( + "/get_dummy_dataset", + json=example_get_dummy_dataset, + headers=new_headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": f"User {fake_user} does not " + + "exist. Please, verify the client object initialisation." + } + def test_smartnoise_query(self) -> None: """Test smartnoise-sql query""" with TestClient(app, headers=self.headers) as client: @@ -355,7 +372,7 @@ def test_smartnoise_query(self) -> None: assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == { "InvalidQueryException": "" - + "Dataset I_do_not_exist does not exists. " + + "Dataset I_do_not_exist does not exist. " + "Please, verify the client object initialisation." } @@ -401,7 +418,9 @@ def test_dummy_smartnoise_query(self) -> None: with TestClient(app) as client: # Expect to work response = client.post( - "/dummy_smartnoise_query", json=example_dummy_smartnoise_sql + "/dummy_smartnoise_query", + json=example_dummy_smartnoise_sql, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK @@ -410,12 +429,24 @@ def test_dummy_smartnoise_query(self) -> None: assert response_dict["query_response"]["data"][0][0] > 0 assert response_dict["query_response"]["data"][0][0] < 200 + # Should fail: no header + response = client.post( + "/dummy_smartnoise_query", json=example_dummy_smartnoise_sql + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "User None does not exist." + + " Please, verify the client object initialisation." + } + def test_smartnoise_cost(self) -> None: """test_smartnoise_cost""" with TestClient(app) as client: # Expect to work response = client.post( - "/estimate_smartnoise_cost", json=example_smartnoise_sql_cost + "/estimate_smartnoise_cost", + json=example_smartnoise_sql_cost, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK @@ -572,7 +603,9 @@ def test_dummy_opendp_query(self) -> None: with TestClient(app) as client: # Expect to work response = client.post( - "/dummy_opendp_query", json=example_dummy_opendp + "/dummy_opendp_query", + json=example_dummy_opendp, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK response_dict = json.loads(response.content.decode("utf8")) @@ -583,7 +616,9 @@ def test_opendp_cost(self) -> None: with TestClient(app) as client: # Expect to work response = client.post( - "/estimate_opendp_cost", json=example_opendp + "/estimate_opendp_cost", + json=example_opendp, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK diff --git a/server/requirements.txt b/server/requirements.txt index 0b615a40..ab4c45c9 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,11 +1,11 @@ boto3==1.34.115 fastapi==0.109.1 httpx==0.27.0 -numpy==1.26.4 +numpy==1.23.2 opendp==0.8.0 opendp-logger==0.3.0 pandas==2.2.2 pyaml==23.9.5 pymongo==4.6.3 smartnoise-sql==1.0.3 -uvicorn==0.29.0 +uvicorn==0.29.0 \ No newline at end of file From 885cb157a87d3503a9b122714a9dfa63e4f2d678 Mon Sep 17 00:00:00 2001 From: PaulineMauryL Date: Mon, 8 Jul 2024 17:30:19 +0200 Subject: [PATCH 2/4] add test for when user does not have access to data --- .../admin_database/admin_database.py | 10 --- server/lomas_server/tests/test_api.py | 86 +++++++++++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/server/lomas_server/admin_database/admin_database.py b/server/lomas_server/admin_database/admin_database.py index 1284f093..96e55ebc 100644 --- a/server/lomas_server/admin_database/admin_database.py +++ b/server/lomas_server/admin_database/admin_database.py @@ -103,16 +103,6 @@ def wrapper_decorator( ) -> None: user_name = args[0] dataset_name = args[1] - if not self.does_user_exist(user_name): - raise UnauthorizedAccessException( - f"User {user_name} does not exist. " - + "Please, verify the client object initialisation.", - ) - if not self.does_dataset_exist(dataset_name): - raise InvalidQueryException( - f"Dataset {dataset_name} does not exist. " - + "Please, verify the client object initialisation.", - ) if not self.has_user_access_to_dataset(user_name, dataset_name): raise UnauthorizedAccessException( f"{user_name} does not have access to {dataset_name}.", diff --git a/server/lomas_server/tests/test_api.py b/server/lomas_server/tests/test_api.py index 7ff37035..e8e67c06 100644 --- a/server/lomas_server/tests/test_api.py +++ b/server/lomas_server/tests/test_api.py @@ -211,6 +211,19 @@ def test_get_dataset_metadata(self) -> None: + "exist. Please, verify the client object initialisation." } + # Expect to fail: user does have access to dataset + other_dataset = "IRIS" + response = client.post( + "/get_dataset_metadata", + json={"dataset_name": other_dataset}, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to {other_dataset}." + } + def test_get_dummy_dataset(self) -> None: """test_get_dummy_dataset""" with TestClient(app) as client: @@ -258,6 +271,23 @@ def test_get_dummy_dataset(self) -> None: ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + # Expect to fail: user does have access to dataset + other_dataset = "IRIS" + response = client.post( + "/get_dummy_dataset", + json={ + "dataset_name": other_dataset, + "dummy_nb_rows": DUMMY_NB_ROWS, + "dummy_seed": 0, + }, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to {other_dataset}." + } + # Expect to fail: user does not exist fake_user = "fake_user" new_headers = self.headers @@ -439,6 +469,20 @@ def test_dummy_smartnoise_query(self) -> None: + " Please, verify the client object initialisation." } + # Should fail: user does not have access to dataset + body = dict(example_dummy_smartnoise_sql) + body["dataset_name"] = "IRIS" + response = client.post( + "/dummy_smartnoise_query", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + } + def test_smartnoise_cost(self) -> None: """test_smartnoise_cost""" with TestClient(app) as client: @@ -454,6 +498,20 @@ def test_smartnoise_cost(self) -> None: assert response_dict["epsilon_cost"] == SMARTNOISE_QUERY_EPSILON assert response_dict["delta_cost"] > SMARTNOISE_QUERY_DELTA + # Should fail: user does not have access to dataset + body = dict(example_smartnoise_sql_cost) + body["dataset_name"] = "IRIS" + response = client.post( + "/estimate_smartnoise_cost", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + } + def test_opendp_query(self) -> None: # pylint: disable=R0915 """test_opendp_query""" enable_logging() @@ -611,6 +669,20 @@ def test_dummy_opendp_query(self) -> None: response_dict = json.loads(response.content.decode("utf8")) assert response_dict["query_response"] > 0 + # Should fail: user does not have access to dataset + body = dict(example_dummy_opendp) + body["dataset_name"] = "IRIS" + response = client.post( + "/dummy_opendp_query", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + } + def test_opendp_cost(self) -> None: """test_opendp_cost""" with TestClient(app) as client: @@ -626,6 +698,20 @@ def test_opendp_cost(self) -> None: assert response_dict["epsilon_cost"] > 0.1 assert response_dict["delta_cost"] == 0 + # Should fail: user does not have access to dataset + body = dict(example_opendp) + body["dataset_name"] = "IRIS" + response = client.post( + "/estimate_opendp_cost", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + } + def test_get_initial_budget(self) -> None: """test_get_initial_budget""" with TestClient(app, headers=self.headers) as client: From 2478fc15c355757bd0a9834c5c0b9a1bb3fa1c25 Mon Sep 17 00:00:00 2001 From: PaulineMauryL Date: Fri, 12 Jul 2024 11:54:24 +0200 Subject: [PATCH 3/4] fix merge conflicts --- server/lomas_server/routes_dp.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/lomas_server/routes_dp.py b/server/lomas_server/routes_dp.py index a455a547..a95f7b2f 100644 --- a/server/lomas_server/routes_dp.py +++ b/server/lomas_server/routes_dp.py @@ -495,6 +495,7 @@ def diffprivlib_query_handler( ) def dummy_diffprivlib_query_handler( query_json: DummyDiffPrivLibInp = Body(example_dummy_diffprivlib), + user_name: str = Header(None), ): """ Handles queries on dummy datasets for the DiffPrivLib library. @@ -528,6 +529,14 @@ def dummy_diffprivlib_query_handler( """ from app import app # pylint: disable=C0415 + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) + ds_private_dataset = get_dummy_dataset_for_query( app.state.admin_database, query_json ) @@ -553,6 +562,7 @@ def dummy_diffprivlib_query_handler( ) def estimate_diffprivlib_cost( query_json: DiffPrivLibInp = Body(example_diffprivlib), + user_name: str = Header(None), ): """ Estimates the privacy loss budget cost of an DiffPrivLib query. @@ -583,6 +593,14 @@ def estimate_diffprivlib_cost( - delta_cost (float): The estimated delta cost. """ from app import app # pylint: disable=C0415 + + dataset_name = query_json.dataset_name + if not app.state.admin_database.has_user_access_to_dataset( + user_name, dataset_name + ): + raise UnauthorizedAccessException( + f"{user_name} does not have access to {dataset_name}.", + ) try: response = app.state.query_handler.estimate_cost( From 7b5f48953e55b149692fda7eeb7d35a39a0a2d17 Mon Sep 17 00:00:00 2001 From: PaulineMauryL Date: Fri, 12 Jul 2024 12:10:45 +0200 Subject: [PATCH 4/4] add verification and test + diffprivlib --- server/lomas_server/routes_admin.py | 33 +++++++++-------- server/lomas_server/routes_dp.py | 37 ++++++++++--------- .../tests/test_api_diffprivlib.py | 36 +++++++++++++++++- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/server/lomas_server/routes_admin.py b/server/lomas_server/routes_admin.py index 7c0c7b52..345bdf0e 100644 --- a/server/lomas_server/routes_admin.py +++ b/server/lomas_server/routes_admin.py @@ -29,17 +29,19 @@ async def root(): # Get server state @router.get("/state", tags=["ADMIN_USER"]) async def get_state( + request: Request, user_name: str = Header(None), ) -> JSONResponse: """Returns the current state dict of this server instance. Args: + request (Request): Raw request object user_name (str, optional): The user name. Defaults to Header(None). Returns: JSONResponse: The state of the server instance. """ - from app import app # pylint: disable=C0415 + app = request.app return JSONResponse( content={ @@ -54,15 +56,16 @@ async def get_state( dependencies=[Depends(server_live)], tags=["ADMIN_USER"], ) -async def get_memory_usage() -> JSONResponse: +async def get_memory_usage(request: Request) -> JSONResponse: """Return the dataset store object memory usage Args: + request (Request): Raw request object user_name (str, optional): The user name. Defaults to Header(None). Returns: JSONResponse: with DatasetStore object memory usage """ - from app import app # pylint: disable=C0415 + app = request.app return JSONResponse( content={ @@ -78,7 +81,7 @@ async def get_memory_usage() -> JSONResponse: tags=["USER_METADATA"], ) def get_dataset_metadata( - _request: Request, + request: Request, query_json: GetDbData = Body(example_get_admin_db_data), user_name: str = Header(None), ) -> JSONResponse: @@ -100,7 +103,7 @@ def get_dataset_metadata( JSONResponse: The metadata dictionary for the specified dataset_name. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -130,7 +133,7 @@ def get_dataset_metadata( tags=["USER_DUMMY"], ) def get_dummy_dataset( - _request: Request, + request: Request, query_json: GetDummyDataset = Body(example_get_dummy_dataset), user_name: str = Header(None), ) -> StreamingResponse: @@ -155,7 +158,7 @@ def get_dummy_dataset( Returns: StreamingResponse: a pd.DataFrame representing the dummy dataset. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -188,7 +191,7 @@ def get_dummy_dataset( tags=["USER_BUDGET"], ) def get_initial_budget( - _request: Request, + request: Request, query_json: GetDbData = Body(example_get_admin_db_data), user_name: str = Header(None), ) -> JSONResponse: @@ -217,7 +220,7 @@ def get_initial_budget( - initial_epsilon (float): initial epsilon budget. - initial_delta (float): initial delta budget. """ - from app import app # pylint: disable=C0415 + app = request.app try: ( @@ -246,7 +249,7 @@ def get_initial_budget( tags=["USER_BUDGET"], ) def get_total_spent_budget( - _request: Request, + request: Request, query_json: GetDbData = Body(example_get_admin_db_data), user_name: str = Header(None), ) -> JSONResponse: @@ -275,7 +278,7 @@ def get_total_spent_budget( - total_spent_epsilon (float): total spent epsilon budget. - total_spent_delta (float): total spent delta budget. """ - from app import app # pylint: disable=C0415 + app = request.app try: ( @@ -304,7 +307,7 @@ def get_total_spent_budget( tags=["USER_BUDGET"], ) def get_remaining_budget( - _request: Request, + request: Request, query_json: GetDbData = Body(example_get_admin_db_data), user_name: str = Header(None), ) -> JSONResponse: @@ -333,7 +336,7 @@ def get_remaining_budget( - remaining_epsilon (float): remaining epsilon budget. - remaining_delta (float): remaining delta budget. """ - from app import app # pylint: disable=C0415 + app = request.app try: rem_epsilon, rem_delta = app.state.admin_database.get_remaining_budget( @@ -359,7 +362,7 @@ def get_remaining_budget( tags=["USER_BUDGET"], ) def get_user_previous_queries( - _request: Request, + request: Request, query_json: GetDbData = Body(example_get_admin_db_data), user_name: str = Header(None), ) -> JSONResponse: @@ -389,7 +392,7 @@ def get_user_previous_queries( - previous_queries (list[dict]): a list of dictionaries containing the previous queries. """ - from app import app # pylint: disable=C0415 + app = request.app try: previous_queries = app.state.admin_database.get_user_previous_queries( diff --git a/server/lomas_server/routes_dp.py b/server/lomas_server/routes_dp.py index a95f7b2f..68e59b2a 100644 --- a/server/lomas_server/routes_dp.py +++ b/server/lomas_server/routes_dp.py @@ -38,7 +38,7 @@ tags=["USER_QUERY"], ) def smartnoise_sql_handler( - _request: Request, + request: Request, query_json: SNSQLInp = Body(example_smartnoise_sql), user_name: str = Header(None), ) -> JSONResponse: @@ -84,7 +84,7 @@ def smartnoise_sql_handler( - spent_delta (float): The amount of delta budget spent for the query. """ - from app import app # pylint: disable=C0415 + app = request.app try: response = app.state.query_handler.handle_query( @@ -105,7 +105,7 @@ def smartnoise_sql_handler( tags=["USER_DUMMY"], ) def dummy_smartnoise_sql_handler( - _request: Request, + request: Request, query_json: DummySNSQLInp = Body(example_dummy_smartnoise_sql), user_name: str = Header(None), ) -> JSONResponse: @@ -147,7 +147,7 @@ def dummy_smartnoise_sql_handler( - query_response (pd.DataFrame): a DataFrame containing the query response. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -181,7 +181,7 @@ def dummy_smartnoise_sql_handler( tags=["USER_QUERY"], ) def estimate_smartnoise_cost( - _request: Request, + request: Request, query_json: SNSQLInpCost = Body(example_smartnoise_sql_cost), user_name: str = Header(None), ) -> JSONResponse: @@ -214,7 +214,7 @@ def estimate_smartnoise_cost( - epsilon_cost (float): The estimated epsilon cost. - delta_cost (float): The estimated delta cost. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -241,7 +241,7 @@ def estimate_smartnoise_cost( "/opendp_query", dependencies=[Depends(server_live)], tags=["USER_QUERY"] ) def opendp_query_handler( - _request: Request, + request: Request, query_json: OpenDPInp = Body(example_opendp), user_name: str = Header(None), ) -> JSONResponse: @@ -283,7 +283,7 @@ def opendp_query_handler( - spent_delta (float): The amount of delta budget spent for the query. """ - from app import app # pylint: disable=C0415 + app = request.app try: response = app.state.query_handler.handle_query( @@ -303,7 +303,7 @@ def opendp_query_handler( tags=["USER_DUMMY"], ) def dummy_opendp_query_handler( - _request: Request, + request: Request, query_json: DummyOpenDPInp = Body(example_dummy_opendp), user_name: str = Header(None), ) -> JSONResponse: @@ -342,7 +342,7 @@ def dummy_opendp_query_handler( - query_response (pd.DataFrame): a DataFrame containing the query response. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -378,7 +378,7 @@ def dummy_opendp_query_handler( tags=["USER_QUERY"], ) def estimate_opendp_cost( - _request: Request, + request: Request, query_json: OpenDPInp = Body(example_opendp), user_name: str = Header(None), ) -> JSONResponse: @@ -405,7 +405,7 @@ def estimate_opendp_cost( - epsilon_cost (float): The estimated epsilon cost. - delta_cost (float): The estimated delta cost. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -434,6 +434,7 @@ def estimate_opendp_cost( tags=["USER_QUERY"], ) def diffprivlib_query_handler( + request: Request, query_json: DiffPrivLibInp = Body(example_diffprivlib), user_name: str = Header(None), ): @@ -474,7 +475,7 @@ def diffprivlib_query_handler( - spent_delta (float): The amount of delta budget spent for the query. """ - from app import app # pylint: disable=C0415 + app = request.app try: response = app.state.query_handler.handle_query( @@ -494,6 +495,7 @@ def diffprivlib_query_handler( tags=["USER_DUMMY"], ) def dummy_diffprivlib_query_handler( + request: Request, query_json: DummyDiffPrivLibInp = Body(example_dummy_diffprivlib), user_name: str = Header(None), ): @@ -527,7 +529,7 @@ def dummy_diffprivlib_query_handler( - query_response (pd.DataFrame): a DataFrame containing the query response. """ - from app import app # pylint: disable=C0415 + app = request.app dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( @@ -536,7 +538,7 @@ def dummy_diffprivlib_query_handler( raise UnauthorizedAccessException( f"{user_name} does not have access to {dataset_name}.", ) - + ds_private_dataset = get_dummy_dataset_for_query( app.state.admin_database, query_json ) @@ -561,6 +563,7 @@ def dummy_diffprivlib_query_handler( tags=["USER_QUERY"], ) def estimate_diffprivlib_cost( + request: Request, query_json: DiffPrivLibInp = Body(example_diffprivlib), user_name: str = Header(None), ): @@ -592,8 +595,8 @@ def estimate_diffprivlib_cost( - epsilon_cost (float): The estimated epsilon cost. - delta_cost (float): The estimated delta cost. """ - from app import app # pylint: disable=C0415 - + app = request.app + dataset_name = query_json.dataset_name if not app.state.admin_database.has_user_access_to_dataset( user_name, dataset_name diff --git a/server/lomas_server/tests/test_api_diffprivlib.py b/server/lomas_server/tests/test_api_diffprivlib.py index 079a4890..94db1846 100644 --- a/server/lomas_server/tests/test_api_diffprivlib.py +++ b/server/lomas_server/tests/test_api_diffprivlib.py @@ -324,7 +324,9 @@ def test_dummy_diffprivlib_query(self) -> None: with TestClient(app) as client: # Expect to work response = client.post( - "/dummy_diffprivlib_query", json=example_dummy_diffprivlib + "/dummy_diffprivlib_query", + json=example_dummy_diffprivlib, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK @@ -332,15 +334,45 @@ def test_dummy_diffprivlib_query(self) -> None: assert response_dict["query_response"]["score"] > 0 assert response_dict["query_response"]["model"] + # Expect to fail: user does have access to dataset + body = dict(example_dummy_diffprivlib) + body["dataset_name"] = "IRIS" + response = client.post( + "/dummy_diffprivlib_query", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + } + def test_diffprivlib_cost(self) -> None: """test_diffprivlib_cost""" with TestClient(app) as client: # Expect to work response = client.post( - "/estimate_diffprivlib_cost", json=example_diffprivlib + "/estimate_diffprivlib_cost", + json=example_diffprivlib, + headers=self.headers, ) assert response.status_code == status.HTTP_200_OK response_dict = json.loads(response.content.decode("utf8")) assert response_dict["epsilon_cost"] == 1.5 assert response_dict["delta_cost"] == 0 + + # Expect to fail: user does have access to dataset + body = dict(example_diffprivlib) + body["dataset_name"] = "IRIS" + response = client.post( + "/estimate_diffprivlib_cost", + json=body, + headers=self.headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json() == { + "UnauthorizedAccessException": "" + + f"{self.user_name} does not have access to IRIS." + }