diff --git a/app/api/catalog.py b/app/api/catalog.py index 82c1d87..a8e82a1 100644 --- a/app/api/catalog.py +++ b/app/api/catalog.py @@ -22,6 +22,9 @@ async def get_table_profile( conn: duckdb.DuckDBPyConnection = Depends(_get_db), ): """Get profiler data for a specific table.""" + # Check table-level access + if not can_access_table(user, table_name, conn): + raise HTTPException(status_code=403, detail=f"Access denied to table '{table_name}'") repo = ProfileRepository(conn) profile = repo.get(table_name) if not profile: @@ -100,6 +103,9 @@ async def refresh_profile( conn: duckdb.DuckDBPyConnection = Depends(_get_db), ): """Re-generate profile for a table on demand.""" + # Check table-level access + if not can_access_table(user, table_name, conn): + raise HTTPException(status_code=403, detail=f"Access denied to table '{table_name}'") from src.profiler import profile_table, TableInfo data_dir = _get_data_dir() diff --git a/tests/test_api_complete.py b/tests/test_api_complete.py index 7937303..56595fb 100644 --- a/tests/test_api_complete.py +++ b/tests/test_api_complete.py @@ -53,9 +53,28 @@ class TestCatalog: assert resp.status_code == 200 def test_catalog_profile_not_found(self, client): - resp = client["client"].get("/api/catalog/profile/nonexistent", headers=_h(client["analyst"])) + # Admin can see 404 for truly missing tables (bypasses access control) + resp = client["client"].get("/api/catalog/profile/nonexistent", headers=_h(client["admin"])) assert resp.status_code == 404 + def test_catalog_profile_access_denied_for_analyst(self, client): + # Non-registered (non-public) table returns 403 for analyst + resp = client["client"].get("/api/catalog/profile/private_table", headers=_h(client["analyst"])) + assert resp.status_code == 403 + + def test_catalog_profile_refresh_access_denied_for_analyst(self, client): + # Refresh endpoint also enforces access control + resp = client["client"].post("/api/catalog/profile/private_table/refresh", headers=_h(client["analyst"])) + assert resp.status_code == 403 + + def test_catalog_profile_public_table_accessible_to_analyst(self, client): + # Register a public table — analyst can access its profile (404 since no profile data) + client["client"].post("/api/admin/register-table", + json={"name": "public_table", "source_type": "keboola"}, + headers=_h(client["admin"])) + resp = client["client"].get("/api/catalog/profile/public_table", headers=_h(client["analyst"])) + assert resp.status_code == 404 # access granted, but no profile data yet + # ---- Telegram ----